completers/dism_completer/dism_completer.ps1

# DISM tab completion for PowerShell
# Static catalog of the documented DISM surface, enriched from DISM's own help when the shell is elevated
# (DISM refuses to print help otherwise).
# Usage: . .\dism_completer.ps1

Set-StrictMode -Version 2.0

if (-not (Get-Variable -Name DismCompletionCatalog -Scope Script -ErrorAction Ignore)) {
    $script:DismCompletionCatalog = @{
        Initialized       = $false
        Commands          = @()
        GlobalSwitches    = @()
        OptionSpecs       = $null
        LiveCommandKeys   = @{}
        ExtraSwitchesByKey = @{}
    }
}

function New-DismSwitchSpec {
    param(
        [string]$Token,
        [string]$Description,
        [string]$Kind = 'Flag',
        [string[]]$Values = @(),
        [string[]]$Extensions = @(),
        [string]$Placeholder = '<value>'
    )

    [pscustomobject]@{
        Token       = $Token
        Description = $Description
        Kind        = $Kind
        Values      = @($Values)
        Extensions  = @($Extensions)
        Placeholder = $Placeholder
    }
}

function New-DismCommandSpec {
    param(
        [string]$Token,
        [string]$Description,
        [object[]]$Switches = @(),
        [string]$Kind = 'Flag',
        [string[]]$Values = @(),
        [string]$Placeholder = '<value>'
    )

    [pscustomobject]@{
        Token       = $Token
        Description = $Description
        Kind        = $Kind
        Values      = @($Values)
        Extensions  = @()
        Placeholder = $Placeholder
        Switches    = @($Switches)
    }
}

function Get-DismStaticCatalog {
    $imageExtensions = @('.wim', '.esd', '.swm', '.ffu', '.vhd', '.vhdx')
    $s = @{
        ImageFile            = New-DismSwitchSpec -Token '/ImageFile:' -Description 'Path to the image file (.wim, .esd, .ffu, .vhd, .vhdx).' -Kind 'File' -Extensions $imageExtensions
        SourceImageFile      = New-DismSwitchSpec -Token '/SourceImageFile:' -Description 'Path to the source image file.' -Kind 'File' -Extensions $imageExtensions
        DestinationImageFile = New-DismSwitchSpec -Token '/DestinationImageFile:' -Description 'Path to the destination image file.' -Kind 'File' -Extensions $imageExtensions
        SWMFile              = New-DismSwitchSpec -Token '/SWMFile:' -Description 'Naming pattern and location of split .swm files.' -Kind 'File' -Extensions @('.swm')
        SFUFile              = New-DismSwitchSpec -Token '/SFUFile:' -Description 'Naming pattern and location of split .sfu files.' -Kind 'File' -Extensions @('.sfu')
        Index                = New-DismSwitchSpec -Token '/Index:' -Description 'Index number of the image inside the file (use 1 for FFU and VHD).' -Kind 'Value' -Placeholder '<index>'
        SourceIndex          = New-DismSwitchSpec -Token '/SourceIndex:' -Description 'Index number of the source image.' -Kind 'Value' -Placeholder '<index>'
        Name                 = New-DismSwitchSpec -Token '/Name:' -Description 'Name of the image.' -Kind 'Value' -Placeholder '<image-name>'
        SourceName           = New-DismSwitchSpec -Token '/SourceName:' -Description 'Name of the source image.' -Kind 'Value' -Placeholder '<image-name>'
        DestinationName      = New-DismSwitchSpec -Token '/DestinationName:' -Description 'Name of the destination image.' -Kind 'Value' -Placeholder '<image-name>'
        Description          = New-DismSwitchSpec -Token '/Description:' -Description 'Description of the image.' -Kind 'Value' -Placeholder '<description>'
        MountDir             = New-DismSwitchSpec -Token '/MountDir:' -Description 'Directory the image is (or will be) mounted to.' -Kind 'Directory'
        ApplyDir             = New-DismSwitchSpec -Token '/ApplyDir:' -Description 'Target directory the image is applied to.' -Kind 'Directory'
        CaptureDir           = New-DismSwitchSpec -Token '/CaptureDir:' -Description 'Source directory to capture.' -Kind 'Directory'
        ConfigFile           = New-DismSwitchSpec -Token '/ConfigFile:' -Description 'Configuration file listing capture and compress exclusions.' -Kind 'File' -Extensions @('.ini')
        CheckIntegrity       = New-DismSwitchSpec -Token '/CheckIntegrity' -Description 'Detects and tracks .wim file corruption.'
        Verify               = New-DismSwitchSpec -Token '/Verify' -Description 'Checks for errors and file duplication.'
        NoRpFix              = New-DismSwitchSpec -Token '/NoRpFix' -Description 'Disables the reparse point tag fix.'
        Bootable             = New-DismSwitchSpec -Token '/Bootable' -Description 'Marks a volume image as bootable (WinPE images only).'
        WIMBoot              = New-DismSwitchSpec -Token '/WIMBoot' -Description 'WIMBoot configuration (Windows 8.1 images only).'
        EA                   = New-DismSwitchSpec -Token '/EA' -Description 'Captures or applies extended attributes.'
        Compress             = New-DismSwitchSpec -Token '/Compress:' -Description 'Compression type for the capture or export.' -Kind 'Enum' -Values @('max', 'fast', 'none', 'recovery')
        FileSize             = New-DismSwitchSpec -Token '/FileSize:' -Description 'Maximum size in MB of each split file.' -Kind 'Value' -Placeholder '<MB-size>'
        PackagePath          = New-DismSwitchSpec -Token '/PackagePath:' -Description 'Path to a .cab/.msu package or a folder of packages.' -Kind 'File' -Extensions @('.cab', '.msu', '.appx', '.appxbundle', '.msix', '.msixbundle', '.spp')
        PackageName          = New-DismSwitchSpec -Token '/PackageName:' -Description 'Name of the package as listed in the image.' -Kind 'Value' -Placeholder '<package-name>'
        FeatureName          = New-DismSwitchSpec -Token '/FeatureName:' -Description 'Name of the feature as listed by /Get-Features.' -Kind 'Value' -Placeholder '<feature-name>'
        CapabilityName       = New-DismSwitchSpec -Token '/CapabilityName:' -Description 'Name of the capability as listed by /Get-Capabilities.' -Kind 'Value' -Placeholder '<capability-name>'
        Source               = New-DismSwitchSpec -Token '/Source:' -Description 'Location of the files required to restore or repair (for example a mounted Windows folder).' -Kind 'Directory'
        LimitAccess          = New-DismSwitchSpec -Token '/LimitAccess' -Description 'Prevents DISM from contacting Windows Update.'
        Format               = New-DismSwitchSpec -Token '/Format:' -Description 'Report output format.' -Kind 'Enum' -Values @('Table', 'List')
        Driver               = New-DismSwitchSpec -Token '/Driver:' -Description 'Path to a driver .inf file or a folder of drivers.' -Kind 'File' -Extensions @('.inf')
        Distribution         = New-DismSwitchSpec -Token '/Distribution:' -Description 'Path to the distribution share.' -Kind 'Directory'
        CustomDataPath       = New-DismSwitchSpec -Token '/CustomDataPath:' -Description 'Custom data file added to the app package as Custom.dat.' -Kind 'File'
        WimFile              = New-DismSwitchSpec -Token '/WimFile:' -Description 'Path to the WIM file.' -Kind 'File' -Extensions @('.wim')
        ReadOnly             = New-DismSwitchSpec -Token '/ReadOnly' -Description 'Mounts the image with read-only permissions.'
        Commit               = New-DismSwitchSpec -Token '/Commit' -Description 'Saves the changes made to the mounted image.'
        Discard              = New-DismSwitchSpec -Token '/Discard' -Description 'Discards the changes made to the mounted image.'
        ProductCode          = New-DismSwitchSpec -Token '/ProductCode:' -Description 'Product code GUID of an installed MSI application (see /Get-Apps).' -Kind 'Value' -Placeholder '<product-code-GUID>'
        PatchCode            = New-DismSwitchSpec -Token '/PatchCode:' -Description 'Patch code GUID of an installed MSP patch (see /Get-AppPatches).' -Kind 'Value' -Placeholder '<patch-code-GUID>'
        LanguageName         = New-DismSwitchSpec -Token '/LanguageName:' -Description 'Name of the language (repeatable).' -Kind 'Value' -Placeholder '<language>'
        SupportPath          = New-DismSwitchSpec -Token '/SupportPath:' -Description 'Directory holding the Edge support files.' -Kind 'Directory'
        ClassId              = New-DismSwitchSpec -Token '/Class-ID:' -Description 'CLSID of the COM class implementing the recovery management plugin.' -Kind 'Value' -Placeholder '<clsid>'
    }

    $globals = @(
        New-DismSwitchSpec -Token '/?' -Description 'Displays information about available DISM command-line options and arguments.'
        New-DismSwitchSpec -Token '/Get-Help' -Description 'Displays information about available DISM command-line options and arguments (same as /?).'
        New-DismSwitchSpec -Token '/Online' -Description 'Targets the running operating system.'
        New-DismSwitchSpec -Token '/Image:' -Description 'Full path to the root directory of the offline Windows image to service.' -Kind 'Directory'
        New-DismSwitchSpec -Token '/WinDir:' -Description 'Path to the Windows directory relative to the image path (used with /Image).' -Kind 'Directory'
        New-DismSwitchSpec -Token '/SysDriveDir:' -Description 'Path to the location of the BootMgr files (Windows PE servicing).' -Kind 'Directory'
        New-DismSwitchSpec -Token '/LogPath:' -Description 'Full path and file name to log to (default %WINDIR%\Logs\Dism\dism.log).' -Kind 'File' -Extensions @('.log')
        New-DismSwitchSpec -Token '/LogLevel:' -Description 'Maximum output level shown in the logs: 1 errors, 2 warnings, 3 informational, 4 debug.' -Kind 'Enum' -Values @('1', '2', '3', '4')
        New-DismSwitchSpec -Token '/ScratchDir:' -Description 'Local temporary directory used when extracting files during servicing.' -Kind 'Directory'
        New-DismSwitchSpec -Token '/Quiet' -Description 'Turns off information and progress output to the console.'
        New-DismSwitchSpec -Token '/NoRestart' -Description 'Suppresses reboot and the restart prompt.'
        New-DismSwitchSpec -Token '/English' -Description 'Displays command-line output in English.'
        $s.Format
    )

    $commands = @(
        # Image management
        New-DismCommandSpec -Token '/Get-ImageInfo' -Description 'Displays information about the images in a .wim, .ffu, .vhd or .vhdx file.' -Switches @($s.ImageFile, $s.Index, $s.Name)
        New-DismCommandSpec -Token '/Get-MountedImageInfo' -Description 'Lists the images that are currently mounted.'
        New-DismCommandSpec -Token '/Mount-Image' -Description 'Mounts an image to a directory so it is available for servicing.' -Switches @($s.ImageFile, $s.Index, $s.Name, $s.MountDir,
            (New-DismSwitchSpec -Token '/ReadOnly' -Description 'Mounts the image with read-only permissions.'),
            (New-DismSwitchSpec -Token '/Optimize' -Description 'Reduces initial mount time.'),
            $s.CheckIntegrity)
        New-DismCommandSpec -Token '/Unmount-Image' -Description 'Unmounts an image and commits or discards the changes.' -Switches @($s.MountDir,
            (New-DismSwitchSpec -Token '/Commit' -Description 'Saves the changes made to the mounted image.'),
            (New-DismSwitchSpec -Token '/Discard' -Description 'Discards the changes made to the mounted image.'),
            $s.CheckIntegrity,
            (New-DismSwitchSpec -Token '/Append' -Description 'Adds the modified image to the .wim file instead of overwriting the original image.'))
        New-DismCommandSpec -Token '/Remount-Image' -Description 'Remounts a mounted image that has become inaccessible.' -Switches @($s.MountDir)
        New-DismCommandSpec -Token '/Commit-Image' -Description 'Applies the changes made to the mounted image without unmounting it.' -Switches @($s.MountDir, $s.CheckIntegrity,
            (New-DismSwitchSpec -Token '/Append' -Description 'Adds the modified image to the .wim file instead of overwriting the original image.'))
        New-DismCommandSpec -Token '/Cleanup-Mountpoints' -Description 'Deletes the resources associated with corrupted mounted images.'
        New-DismCommandSpec -Token '/Apply-Image' -Description 'Applies a .wim or split .swm image to a specified directory.' -Switches @($s.ImageFile, $s.SWMFile, $s.ApplyDir, $s.Index, $s.Name, $s.CheckIntegrity, $s.Verify, $s.NoRpFix,
            (New-DismSwitchSpec -Token '/ConfirmTrustedFile' -Description 'Validates the image for Trusted Desktop (WinPE 4.0 or later).'),
            $s.WIMBoot,
            (New-DismSwitchSpec -Token '/Compact' -Description 'Applies the image in compact mode, saving drive space.'),
            $s.EA)
        New-DismCommandSpec -Token '/Capture-Image' -Description 'Captures an image of a drive to a new .wim file.' -Switches @($s.ImageFile, $s.CaptureDir, $s.Name, $s.Description, $s.ConfigFile, $s.Compress, $s.Bootable, $s.WIMBoot, $s.CheckIntegrity, $s.Verify, $s.NoRpFix, $s.EA)
        New-DismCommandSpec -Token '/Capture-CustomImage' -Description 'Captures incremental file changes to a custom.wim for a WIMBoot image.' -Switches @($s.CaptureDir, $s.ConfigFile, $s.CheckIntegrity, $s.Verify,
            (New-DismSwitchSpec -Token '/ConfirmTrustedFile' -Description 'Validates the image for Trusted Desktop (WinPE 4.0 or later).'))
        New-DismCommandSpec -Token '/Append-Image' -Description 'Adds an additional image to a .wim file.' -Switches @($s.ImageFile, $s.CaptureDir, $s.Name, $s.Description, $s.ConfigFile, $s.Bootable, $s.WIMBoot, $s.CheckIntegrity, $s.Verify, $s.NoRpFix)
        New-DismCommandSpec -Token '/Export-Image' -Description 'Exports a copy of the specified image to another file.' -Switches @($s.SourceImageFile, $s.SourceIndex, $s.SourceName, $s.DestinationImageFile, $s.DestinationName, $s.SWMFile, $s.Compress, $s.Bootable, $s.WIMBoot, $s.CheckIntegrity)
        New-DismCommandSpec -Token '/Delete-Image' -Description 'Deletes a volume image from a .wim file with multiple images.' -Switches @($s.ImageFile, $s.Index, $s.Name, $s.CheckIntegrity)
        New-DismCommandSpec -Token '/Split-Image' -Description 'Splits an existing .wim file into multiple read-only .swm files.' -Switches @($s.ImageFile, $s.SWMFile, $s.FileSize, $s.CheckIntegrity)
        New-DismCommandSpec -Token '/List-Image' -Description 'Lists the files and folders in a specified image.' -Switches @($s.ImageFile, $s.Index, $s.Name)
        New-DismCommandSpec -Token '/Optimize-Image' -Description 'Optimizes an offline image (last command before applying it to a device).' -Switches @(
            (New-DismSwitchSpec -Token '/Boot' -Description 'Reduces the online configuration time the OS spends during boot.'),
            $s.WIMBoot)
        New-DismCommandSpec -Token '/Apply-FFU' -Description 'Applies a full flash update (.ffu) or split FFU image to a physical drive.' -Switches @($s.ImageFile,
            (New-DismSwitchSpec -Token '/ApplyDrive:' -Description 'Physical drive to image (for example \\.\PhysicalDrive0).' -Kind 'Value' -Placeholder '\\.\PhysicalDrive<n>'),
            $s.SFUFile)
        New-DismCommandSpec -Token '/Capture-FFU' -Description 'Captures an image of a physical drive to a new .ffu file.' -Switches @($s.ImageFile,
            (New-DismSwitchSpec -Token '/CaptureDrive:' -Description 'Physical drive to capture (for example \\.\PhysicalDrive0).' -Kind 'Value' -Placeholder '\\.\PhysicalDrive<n>'),
            $s.Name, $s.Description,
            (New-DismSwitchSpec -Token '/PlatformIds:' -Description 'Semicolon-separated platform ids to add to the image.' -Kind 'Value' -Placeholder '<platform-ids>'),
            (New-DismSwitchSpec -Token '/Compress:' -Description 'Compression used when capturing (use none when the FFU will be split).' -Kind 'Enum' -Values @('default', 'none')))
        New-DismCommandSpec -Token '/Split-FFU' -Description 'Splits an existing .ffu file into multiple read-only .sfu files.' -Switches @($s.ImageFile, $s.SFUFile, $s.FileSize, $s.CheckIntegrity)
        New-DismCommandSpec -Token '/Optimize-FFU' -Description 'Optimizes an FFU image so it deploys faster and to differently-sized disks.' -Switches @($s.ImageFile,
            (New-DismSwitchSpec -Token '/PartitionNumber:' -Description 'Partition to optimize (defaults to the OS partition).' -Kind 'Value' -Placeholder '<partition-number>'))
        New-DismCommandSpec -Token '/Apply-CustomDataImage' -Description 'Dehydrates files contained in a custom data image.' -Switches @(
            (New-DismSwitchSpec -Token '/CustomDataImage:' -Description 'Path to the custom data image (.wim).' -Kind 'File' -Extensions @('.wim')),
            (New-DismSwitchSpec -Token '/ImagePath:' -Description 'Path to the root of the applied Windows image.' -Kind 'Directory'),
            (New-DismSwitchSpec -Token '/SingleInstance' -Description 'Single-instances the files in the custom data image.'))
        New-DismCommandSpec -Token '/Apply-SiloedPackage' -Description 'Applies one or more siloed provisioning packages (.spp) to an applied image.' -Switches @(
            (New-DismSwitchSpec -Token '/PackagePath:' -Description 'Path of a siloed provisioning package file.' -Kind 'File' -Extensions @('.spp')),
            (New-DismSwitchSpec -Token '/ImagePath:' -Description 'Path of the Windows image where the SPP is applied.' -Kind 'Directory'))
        New-DismCommandSpec -Token '/Get-WIMBootEntry' -Description 'Displays WIMBoot configuration entries for the specified volume.' -Switches @(
            (New-DismSwitchSpec -Token '/Path:' -Description 'Disk volume of the WIMBoot configuration.' -Kind 'Directory'))
        New-DismCommandSpec -Token '/Update-WIMBootEntry' -Description 'Updates a WIMBoot configuration entry with a renamed or moved image file.' -Switches @(
            (New-DismSwitchSpec -Token '/Path:' -Description 'Disk volume of the WIMBoot configuration.' -Kind 'Directory'),
            (New-DismSwitchSpec -Token '/DataSourceID:' -Description 'Data source id as displayed by /Get-WIMBootEntry.' -Kind 'Value' -Placeholder '<data-source-id>'),
            $s.ImageFile)
        New-DismCommandSpec -Token '/Mount-Wim' -Description 'Mounts an image from a WIM file.' -Switches @($s.WimFile, $s.Index, $s.Name, $s.MountDir, $s.ReadOnly, $s.EA)
        New-DismCommandSpec -Token '/Unmount-Wim' -Description 'Unmounts a mounted WIM image.' -Switches @($s.MountDir, $s.Commit, $s.Discard, $s.EA)
        New-DismCommandSpec -Token '/Get-WimInfo' -Description 'Displays information about images in a WIM file.' -Switches @($s.WimFile, $s.Index, $s.Name)
        New-DismCommandSpec -Token '/Get-MountedWimInfo' -Description 'Displays information about mounted WIM images.'
        New-DismCommandSpec -Token '/Commit-Wim' -Description 'Saves changes to a mounted WIM image.' -Switches @($s.MountDir, $s.EA)
        New-DismCommandSpec -Token '/Remount-Wim' -Description 'Recovers an orphaned WIM mount directory.' -Switches @($s.MountDir)
        New-DismCommandSpec -Token '/Cleanup-Wim' -Description 'Deletes resources associated with mounted WIM images that are corrupted.'
        # OS package servicing
        New-DismCommandSpec -Token '/Get-Packages' -Description 'Displays basic information about all packages in the image.' -Switches @($s.Format)
        New-DismCommandSpec -Token '/Get-PackageInfo' -Description 'Displays detailed information about a package (.cab).' -Switches @($s.PackageName, $s.PackagePath)
        New-DismCommandSpec -Token '/Add-Package' -Description 'Installs a .cab or .msu package in the image.' -Switches @($s.PackagePath,
            (New-DismSwitchSpec -Token '/IgnoreCheck' -Description 'Skips the applicability check of each package.'),
            (New-DismSwitchSpec -Token '/PreventPending' -Description 'Skips the installation when the image has pending online actions.'))
        New-DismCommandSpec -Token '/Remove-Package' -Description 'Removes a .cab package from the image.' -Switches @($s.PackageName, $s.PackagePath)
        New-DismCommandSpec -Token '/Get-Features' -Description 'Displays basic information about all features in a package or the image.' -Switches @($s.PackageName, $s.PackagePath, $s.Format)
        New-DismCommandSpec -Token '/Get-FeatureInfo' -Description 'Displays detailed information about a feature.' -Switches @($s.FeatureName, $s.PackageName, $s.PackagePath)
        New-DismCommandSpec -Token '/Enable-Feature' -Description 'Enables or updates the specified feature in the image.' -Switches @($s.FeatureName, $s.PackageName, $s.PackagePath, $s.Source, $s.LimitAccess,
            (New-DismSwitchSpec -Token '/All' -Description 'Enables all parent features of the specified feature.'))
        New-DismCommandSpec -Token '/Disable-Feature' -Description 'Disables the specified feature in the image.' -Switches @($s.FeatureName, $s.PackageName, $s.PackagePath,
            (New-DismSwitchSpec -Token '/Remove' -Description 'Removes the feature payload without removing its manifest.'))
        New-DismCommandSpec -Token '/Cleanup-Image' -Description 'Performs cleanup or recovery operations on the image.' -Switches @(
            (New-DismSwitchSpec -Token '/CheckHealth' -Description 'Checks whether the image has been flagged as corrupted and whether it can be repaired.'),
            (New-DismSwitchSpec -Token '/ScanHealth' -Description 'Scans the image for component store corruption.'),
            (New-DismSwitchSpec -Token '/RestoreHealth' -Description 'Scans the image for corruption and performs repair operations automatically.'),
            $s.Source, $s.LimitAccess,
            (New-DismSwitchSpec -Token '/StartComponentCleanup' -Description 'Cleans up superseded components and reduces the size of the component store.'),
            (New-DismSwitchSpec -Token '/ResetBase' -Description 'Resets the base of superseded components (with /StartComponentCleanup).'),
            (New-DismSwitchSpec -Token '/Defer' -Description 'Defers long-running cleanup to the next automatic maintenance (with /ResetBase).'),
            (New-DismSwitchSpec -Token '/SPSuperseded' -Description 'Removes backup files created during a service pack installation.'),
            (New-DismSwitchSpec -Token '/HideSP' -Description 'Prevents the service pack from being listed in Installed Updates (with /SPSuperseded).'),
            (New-DismSwitchSpec -Token '/AnalyzeComponentStore' -Description 'Creates a report of the component store.'),
            (New-DismSwitchSpec -Token '/RevertPendingActions' -Description 'Reverts all pending actions from previous servicing operations (offline recovery only).'))
        New-DismCommandSpec -Token '/Add-Language' -Description 'Adds the best-matched package(s) for the given language(s).' -Switches @($s.LanguageName, $s.Source, $s.LimitAccess)
        New-DismCommandSpec -Token '/Remove-Language' -Description 'Removes the best-matched package(s) for the given language(s).' -Switches @($s.LanguageName)
        # Capabilities
        New-DismCommandSpec -Token '/Get-Capabilities' -Description 'Displays the capabilities in the image.' -Switches @($s.Format, $s.Source, $s.LimitAccess)
        New-DismCommandSpec -Token '/Get-CapabilityInfo' -Description 'Displays detailed information about a capability.' -Switches @($s.CapabilityName, $s.Source, $s.LimitAccess)
        New-DismCommandSpec -Token '/Add-Capability' -Description 'Adds a capability to the image.' -Switches @($s.CapabilityName, $s.Source, $s.LimitAccess)
        New-DismCommandSpec -Token '/Remove-Capability' -Description 'Removes a capability from the image.' -Switches @($s.CapabilityName)
        New-DismCommandSpec -Token '/Export-Source' -Description 'Exports the source files of the capabilities in the image.' -Switches @($s.Source,
            (New-DismSwitchSpec -Token '/Target:' -Description 'Destination folder for the exported source.' -Kind 'Directory'),
            $s.CapabilityName)
        # Drivers
        New-DismCommandSpec -Token '/Get-Drivers' -Description 'Displays basic information about driver packages in the image.' -Switches @(
            (New-DismSwitchSpec -Token '/All' -Description 'Includes the default (inbox) drivers.'),
            $s.Format)
        New-DismCommandSpec -Token '/Get-DriverInfo' -Description 'Displays detailed information about a driver .inf file.' -Switches @($s.Driver)
        New-DismCommandSpec -Token '/Add-Driver' -Description 'Adds third-party driver packages to an offline image.' -Switches @($s.Driver,
            (New-DismSwitchSpec -Token '/Recurse' -Description 'Installs all drivers found in the folder and its subfolders.'),
            (New-DismSwitchSpec -Token '/ForceUnsigned' -Description 'Adds unsigned drivers to x64-based images.'))
        New-DismCommandSpec -Token '/Remove-Driver' -Description 'Removes third-party drivers from an offline image.' -Switches @($s.Driver)
        New-DismCommandSpec -Token '/Export-Driver' -Description 'Exports all third-party driver packages to a destination path.' -Switches @(
            (New-DismSwitchSpec -Token '/Destination:' -Description 'Folder the driver packages are exported to.' -Kind 'Directory'))
        # Editions
        New-DismCommandSpec -Token '/Get-CurrentEdition' -Description 'Displays the edition of the image.'
        New-DismCommandSpec -Token '/Get-TargetEditions' -Description 'Displays the editions the image can be upgraded to.'
        New-DismCommandSpec -Token '/Set-Edition:' -Description 'Changes the image to a higher edition (/Set-Edition:<target edition>).' -Kind 'Value' -Placeholder '<target-edition>' -Switches @(
            (New-DismSwitchSpec -Token '/ProductKey:' -Description 'Product key for the target edition.' -Kind 'Value' -Placeholder '<product-key>'),
            (New-DismSwitchSpec -Token '/AcceptEula' -Description 'Accepts the licence terms for the target edition.'),
            (New-DismSwitchSpec -Token '/Channel:' -Description 'Path to the licensing channel (.xrm-ms) file.' -Kind 'File' -Extensions @('.xrm-ms')))
        New-DismCommandSpec -Token '/Set-ProductKey:' -Description 'Enters the product key for the current edition (/Set-ProductKey:<key>).' -Kind 'Value' -Placeholder '<product-key>'
        # Unattend and app packages
        New-DismCommandSpec -Token '/Apply-Unattend:' -Description 'Applies an unattend.xml answer file to the image (/Apply-Unattend:<path>).' -Kind 'File' -Placeholder '<unattend.xml>'
        New-DismCommandSpec -Token '/Get-ProvisionedAppxPackages' -Description 'Displays the app packages (.appx/.appxbundle) provisioned in the image.'
        New-DismCommandSpec -Token '/Add-ProvisionedAppxPackage' -Description 'Adds one or more app packages to the image.' -Switches @(
            (New-DismSwitchSpec -Token '/FolderPath:' -Description 'Folder of unpacked app files (main package, dependencies, licence).' -Kind 'Directory'),
            (New-DismSwitchSpec -Token '/PackagePath:' -Description 'App package (.appx, .appxbundle, .msix, .msixbundle).' -Kind 'File' -Extensions @('.appx', '.appxbundle', '.msix', '.msixbundle')),
            (New-DismSwitchSpec -Token '/DependencyPackagePath:' -Description 'Dependency package needed by the app (repeatable).' -Kind 'File' -Extensions @('.appx', '.appxbundle', '.msix', '.msixbundle')),
            (New-DismSwitchSpec -Token '/LicensePath:' -Description 'Location of the .xml file containing the application licence.' -Kind 'File' -Extensions @('.xml')),
            (New-DismSwitchSpec -Token '/SkipLicense' -Description 'Skips the licence for apps that do not require one (sideloading-enabled computers).'),
            $s.CustomDataPath,
            (New-DismSwitchSpec -Token '/Region:' -Description 'Regions the package is provisioned for: all, or a semicolon-separated list of ISO 3166-1 codes.' -Kind 'Enum' -Values @('all')),
            (New-DismSwitchSpec -Token '/StubPackageOption:' -Description 'Stub preference of the package.' -Kind 'Enum' -Values @('installstub', 'installfull')))
        New-DismCommandSpec -Token '/Remove-ProvisionedAppxPackage' -Description 'Removes provisioning for an app package from the image.' -Switches @($s.PackageName)
        New-DismCommandSpec -Token '/Optimize-ProvisionedAppxPackages' -Description 'Optimizes the total file size of provisioned packages using hardlinks (offline only).'
        New-DismCommandSpec -Token '/Set-ProvisionedAppxDataFile' -Description 'Adds a custom data file into a provisioned app package.' -Switches @($s.CustomDataPath, $s.PackageName)
        New-DismCommandSpec -Token '/Get-DefaultAppAssociations' -Description 'Displays the default application associations in the image.'
        New-DismCommandSpec -Token '/Export-DefaultAppAssociations:' -Description 'Exports the default application associations to an XML file.' -Kind 'File' -Placeholder '<path.xml>'
        New-DismCommandSpec -Token '/Import-DefaultAppAssociations:' -Description 'Imports default application associations from an XML file.' -Kind 'File' -Placeholder '<path.xml>'
        New-DismCommandSpec -Token '/Remove-DefaultAppAssociations' -Description 'Removes the default application associations from the image.'
        New-DismCommandSpec -Token '/Set-NonRemovableAppPolicy' -Description 'Sets a policy that prevents users from removing the app packages of a package family.' -Switches @(
            (New-DismSwitchSpec -Token '/PackageFamily:' -Description 'Package family name the policy applies to.' -Kind 'Value' -Placeholder '<package-family-name>'),
            (New-DismSwitchSpec -Token '/NonRemovable:' -Description '1 makes the package family nonremovable; 0 removes the policy.' -Kind 'Enum' -Values @('0', '1')))
        New-DismCommandSpec -Token '/Get-NonRemovableAppPolicy' -Description 'Lists the package families configured to be nonremovable by enterprise policy.'
        # Provisioning packages
        New-DismCommandSpec -Token '/Add-ProvisioningPackage' -Description 'Adds the applicable payload of a provisioning package to the image.' -Switches @(
            (New-DismSwitchSpec -Token '/PackagePath:' -Description 'Path to the provisioning package (.ppkg).' -Kind 'File' -Extensions @('.ppkg')),
            (New-DismSwitchSpec -Token '/CatalogPath:' -Description 'Path to the catalog file of the provisioning package.' -Kind 'File' -Extensions @('.cat')))
        New-DismCommandSpec -Token '/Get-ProvisioningPackageInfo' -Description 'Displays information about a provisioning package.' -Switches @(
            (New-DismSwitchSpec -Token '/PackagePath:' -Description 'Path to the provisioning package (.ppkg).' -Kind 'File' -Extensions @('.ppkg')))
        # Application (MSI/MSP) servicing - offline images only
        New-DismCommandSpec -Token '/Get-Apps' -Description 'Displays information about all installed MSI applications.' -Switches @($s.Format)
        New-DismCommandSpec -Token '/Get-AppInfo' -Description 'Displays information about a specific installed MSI application.' -Switches @($s.Format, $s.ProductCode)
        New-DismCommandSpec -Token '/Get-AppPatches' -Description 'Displays information about all applied MSP patches for all installed applications.' -Switches @($s.Format, $s.ProductCode)
        New-DismCommandSpec -Token '/Get-AppPatchInfo' -Description 'Displays information about installed MSP patches.' -Switches @($s.Format, $s.PatchCode, $s.ProductCode)
        New-DismCommandSpec -Token '/Check-AppPatch' -Description 'Displays whether MSP patches are applicable to the mounted image.' -Switches @(
            (New-DismSwitchSpec -Token '/PatchLocation:' -Description 'Path to an MSP patch file (repeatable).' -Kind 'File' -Extensions @('.msp')),
            $s.Format)
        New-DismCommandSpec -Token '/Add-App' -Description 'Installs an MSI application into the offline image.' -Switches @(
            (New-DismSwitchSpec -Token '/AppPath:' -Description 'Path to the MSI application.' -Kind 'File' -Extensions @('.msi')))
        New-DismCommandSpec -Token '/Remove-App' -Description 'Uninstalls an MSI application from the offline image.' -Switches @($s.ProductCode)
        New-DismCommandSpec -Token '/Add-AppPatch' -Description 'Installs an MSP patch into the offline image.' -Switches @(
            (New-DismSwitchSpec -Token '/AppPatchPath:' -Description 'Path to the MSP patch.' -Kind 'File' -Extensions @('.msp')))
        New-DismCommandSpec -Token '/Remove-AppPatch' -Description 'Uninstalls an MSP patch from the offline image.' -Switches @($s.PatchCode)
        # Edge servicing
        New-DismCommandSpec -Token '/Add-Edge' -Description 'Adds Edge to the image.' -Switches @($s.SupportPath)
        New-DismCommandSpec -Token '/Add-EdgeBrowser' -Description 'Adds the Edge browser to the image.' -Switches @($s.SupportPath)
        New-DismCommandSpec -Token '/Add-EdgeWebView' -Description 'Adds Edge WebView to the image.' -Switches @($s.SupportPath)
        New-DismCommandSpec -Token '/Remove-Edge' -Description 'Removes Edge from the image.'
        New-DismCommandSpec -Token '/Remove-EdgeBrowser' -Description 'Removes the Edge browser from the image.'
        New-DismCommandSpec -Token '/Remove-EdgeWebView' -Description 'Removes Edge WebView from the image.'
        # International servicing
        New-DismCommandSpec -Token '/Get-Intl' -Description 'Displays information about the international settings and languages.' -Switches @($s.Distribution)
        New-DismCommandSpec -Token '/Set-SysUILang:' -Description 'Sets the system UI language of an offline image (/Set-SysUILang:<language>).' -Kind 'Value' -Placeholder '<language>'
        New-DismCommandSpec -Token '/Set-UILang:' -Description 'Sets the default system UI language (/Set-UILang:<language>).' -Kind 'Value' -Placeholder '<language>'
        New-DismCommandSpec -Token '/Set-UILangFallback:' -Description 'Sets the fallback system UI language (/Set-UILangFallback:<language>).' -Kind 'Value' -Placeholder '<language>'
        New-DismCommandSpec -Token '/Set-SysLocale:' -Description 'Sets the language for non-Unicode programs (/Set-SysLocale:<locale>).' -Kind 'Value' -Placeholder '<locale>'
        New-DismCommandSpec -Token '/Set-UserLocale:' -Description 'Sets the per-user format settings (/Set-UserLocale:<locale>).' -Kind 'Value' -Placeholder '<locale>'
        New-DismCommandSpec -Token '/Set-InputLocale:' -Description 'Sets the input locale and keyboard layout (/Set-InputLocale:<locale>).' -Kind 'Value' -Placeholder '<locale>'
        New-DismCommandSpec -Token '/Set-AllIntl:' -Description 'Sets all international settings to one language (/Set-AllIntl:<language>).' -Kind 'Value' -Placeholder '<language>'
        New-DismCommandSpec -Token '/Set-TimeZone:' -Description 'Sets the default time zone (/Set-TimeZone:<time zone>).' -Kind 'Value' -Placeholder '<time-zone>'
        New-DismCommandSpec -Token '/Set-SKUIntlDefaults:' -Description 'Sets the SKU international defaults for a language (/Set-SKUIntlDefaults:<language>).' -Kind 'Value' -Placeholder '<language>'
        New-DismCommandSpec -Token '/Set-LayeredDriver:' -Description 'Sets the keyboard layered driver (/Set-LayeredDriver:{1..6}).' -Kind 'Enum' -Values @('1', '2', '3', '4', '5', '6')
        New-DismCommandSpec -Token '/Gen-LangIni' -Description 'Generates a new Lang.ini file for Windows Setup.' -Switches @($s.Distribution)
        New-DismCommandSpec -Token '/Set-SetupUILang:' -Description 'Sets the default language used by Windows Setup (/Set-SetupUILang:<language>).' -Kind 'Value' -Placeholder '<language>' -Switches @($s.Distribution)
        # Windows PE servicing
        New-DismCommandSpec -Token '/Get-PESettings' -Description 'Displays the Windows PE settings in the image.'
        New-DismCommandSpec -Token '/Get-ScratchSpace' -Description 'Displays the configured Windows PE scratch space.'
        New-DismCommandSpec -Token '/Get-TargetPath' -Description 'Displays the target path of the Windows PE image.'
        New-DismCommandSpec -Token '/Set-ScratchSpace:' -Description 'Sets the Windows PE scratch space in MB (/Set-ScratchSpace:<size>).' -Kind 'Enum' -Values @('32', '64', '128', '256', '512')
        New-DismCommandSpec -Token '/Set-TargetPath:' -Description 'Sets the location of the Windows PE image on the disk (/Set-TargetPath:<path>).' -Kind 'Value' -Placeholder '<target-path>'
        New-DismCommandSpec -Token '/Enable-Profiling' -Description 'Enables Windows PE file logging (profiling).'
        New-DismCommandSpec -Token '/Disable-Profiling' -Description 'Disables Windows PE file logging (profiling).'
        New-DismCommandSpec -Token '/Get-Profiling' -Description 'Displays whether Windows PE profiling is enabled.'
        # Reserved storage
        New-DismCommandSpec -Token '/Get-ReservedStorageState' -Description 'Displays whether reserved storage is enabled.'
        New-DismCommandSpec -Token '/Set-ReservedStorageState' -Description 'Enables or disables reserved storage.' -Switches @(
            (New-DismSwitchSpec -Token '/State:' -Description 'Reserved storage state.' -Kind 'Enum' -Values @('Enabled', 'Disabled')))
        New-DismCommandSpec -Token '/Enable-ReservedStorage' -Description 'Enables reserved storage.'
        New-DismCommandSpec -Token '/Disable-ReservedStorage' -Description 'Disables reserved storage.'
        # OS uninstall
        New-DismCommandSpec -Token '/Initiate-OSUninstall' -Description 'Initiates the OS uninstall to go back to the previous version of Windows.'
        New-DismCommandSpec -Token '/Remove-OSUninstall' -Description 'Removes the OS uninstall capability from the computer.'
        New-DismCommandSpec -Token '/Get-OSUninstallWindow' -Description 'Displays the number of days after upgrade during which OS uninstall can be performed.'
        New-DismCommandSpec -Token '/Set-OSUninstallWindow' -Description 'Sets the number of days after upgrade during which OS uninstall can be performed.' -Switches @(
            (New-DismSwitchSpec -Token '/Value:' -Description 'Number of days.' -Kind 'Value' -Placeholder '<days>'))
        # Recovery management
        New-DismCommandSpec -Token '/Get-RemoteManagementStatus' -Description 'Shows whether Recovery Remote Management Plugins (RRMPs) are enabled.'
        New-DismCommandSpec -Token '/Set-RemoteManagementStatus' -Description 'Enables or disables Recovery Remote Management Plugins (RRMPs).' -Switches @(
            (New-DismSwitchSpec -Token '/Status:' -Description 'Enable or disable RRMPs.' -Kind 'Enum' -Values @('Enable', 'Disable')))
        New-DismCommandSpec -Token '/Register-RRMP' -Description 'Registers a Recovery Remote Management Plugin (RRMP).' -Switches @(
            (New-DismSwitchSpec -Token '/Binary-Location:' -Description 'Full path of the plugin DLL under System32, as the booted target OS sees it.' -Kind 'Value' -Placeholder '<dll-path>'),
            $s.ClassId,
            (New-DismSwitchSpec -Token '/Capabilities-Required:' -Description 'Capability bitmask that must be satisfied: 0x1 offline OS, 0x2 network, 0x4 internet.' -Kind 'Value' -Placeholder '<mask>'),
            (New-DismSwitchSpec -Token '/Capabilities-Desired:' -Description 'Capability bitmask the plugin would like (defaults to /Capabilities-Required).' -Kind 'Value' -Placeholder '<mask>'),
            (New-DismSwitchSpec -Token '/Threading-Model:' -Description 'COM threading model (default Apartment).' -Kind 'Enum' -Values @('Apartment', 'Free', 'Both')),
            (New-DismSwitchSpec -Token '/Exception-Handling:' -Description 'Exception handling mode (default Default).' -Kind 'Enum' -Values @('Default', 'All', 'Fatal', 'None')))
        New-DismCommandSpec -Token '/Unregister-RRMP' -Description 'Unregisters a Recovery Remote Management Plugin (RRMP).' -Switches @($s.ClassId)
        New-DismCommandSpec -Token '/Get-RRMPInfo' -Description 'Shows the details of a registered Recovery Remote Management Plugin (RRMP).' -Switches @($s.ClassId)
        New-DismCommandSpec -Token '/Get-RRMPs' -Description 'Lists all registered Recovery Remote Management Plugins (RRMPs).'
        New-DismCommandSpec -Token '/Get-RRMPAltitude' -Description 'Shows the altitude of a Recovery Remote Management Plugin (RRMP).' -Switches @($s.ClassId)
        New-DismCommandSpec -Token '/Set-RRMPAltitude' -Description 'Sets the altitude of a Recovery Remote Management Plugin (RRMP).' -Switches @($s.ClassId,
            (New-DismSwitchSpec -Token '/Altitude:' -Description 'Altitude value (unsigned integer); lower altitudes have higher priority.' -Kind 'Value' -Placeholder '<value>'))
        New-DismCommandSpec -Token '/Remove-RRMPAltitude' -Description 'Removes the altitude of a Recovery Remote Management Plugin (RRMP).' -Switches @($s.ClassId)
    )

    [pscustomobject]@{
        GlobalSwitches = @($globals)
        Commands       = @($commands)
    }
}

function Get-DismOptionKey {
    param([string]$Token)

    if ([string]::IsNullOrWhiteSpace($Token)) {
        return $null
    }

    $clean = $Token.Trim('"')
    if ($clean -match '^(/[A-Za-z?][A-Za-z0-9\-]*:?)') {
        return $matches[1].ToLowerInvariant()
    }

    $null
}

function Test-DismElevated {
    try {
        $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
        ([System.Security.Principal.WindowsPrincipal]$identity).IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
    } catch {
        $false
    }
}

function Invoke-DismHelpText {
    param([string[]]$Arguments, [int]$TimeoutMilliseconds = 2000)

    $command = Get-Command -Name 'dism.exe', 'dism' -CommandType Application -ErrorAction Ignore | Select-Object -First 1
    if (-not $command) {
        return @()
    }

    try {
        $startInfo = [System.Diagnostics.ProcessStartInfo]::new()
        $startInfo.FileName = $command.Source
        $startInfo.UseShellExecute = $false
        $startInfo.RedirectStandardInput = $true
        $startInfo.RedirectStandardOutput = $true
        $startInfo.RedirectStandardError = $true
        $startInfo.CreateNoWindow = $true
        foreach ($argument in @($Arguments)) {
            [void]$startInfo.ArgumentList.Add($argument)
        }
        [void]$startInfo.ArgumentList.Add('/?')

        $process = [System.Diagnostics.Process]::new()
        $process.StartInfo = $startInfo
        [void]$process.Start()
        $process.StandardInput.Close()
        $outputTask = $process.StandardOutput.ReadToEndAsync()
        $errorTask = $process.StandardError.ReadToEndAsync()
        if (-not $process.WaitForExit($TimeoutMilliseconds)) {
            try { $process.Kill($true) } catch { Write-Debug -Message $_.Exception.Message }
            return @()
        }

        [void]$errorTask.Result
        $text = ($outputTask.Result -replace '\e\[[0-9;?]*[ -/]*[@-~]', '')
        if ([string]::IsNullOrWhiteSpace($text) -or $text -match 'Error:\s*740' -or $text -match 'Elevated permissions are required') {
            return @()
        }

        @($text -split '\r?\n')
    } catch {
        @()
    }
}

function Get-DismSwitchTokensFromLines {
    param([string[]]$Lines)

    $tokens = foreach ($line in @($Lines)) {
        foreach ($match in [regex]::Matches($line, '(?<!\w)(/[A-Za-z][A-Za-z0-9\-]*:?)(?=[\s\]\}\|,]|$|<)')) {
            $match.Groups[1].Value
        }
    }

    @($tokens | Sort-Object -Unique)
}

function Add-DismLiveSwitchSet {
    param([string]$Key, [string[]]$HelpLines)

    # Enrichment only: tokens the static catalog does not know are added as plain flags under the given key.
    $known = $script:DismCompletionCatalog.OptionSpecs
    $extras = New-Object System.Collections.Generic.List[object]
    foreach ($token in Get-DismSwitchTokensFromLines -Lines $HelpLines) {
        $tokenKey = Get-DismOptionKey -Token $token
        if ($null -eq $tokenKey -or $known.ContainsKey($tokenKey) -or $known.ContainsKey($tokenKey + ':')) {
            continue
        }

        [void]$extras.Add((New-DismSwitchSpec -Token $token -Description "DISM option $token (from live help)."))
    }

    $script:DismCompletionCatalog.ExtraSwitchesByKey[$Key] = @($extras.ToArray())
}

function Initialize-DismCompletionCatalog {
    if ($script:DismCompletionCatalog.Initialized) {
        return
    }

    $static = Get-DismStaticCatalog
    $script:DismCompletionCatalog.Commands = @($static.Commands)
    $script:DismCompletionCatalog.GlobalSwitches = @($static.GlobalSwitches)

    $specs = @{}
    foreach ($switch in @($static.GlobalSwitches)) {
        $specs[(Get-DismOptionKey -Token $switch.Token)] = $switch
    }
    foreach ($command in @($static.Commands)) {
        $specs[(Get-DismOptionKey -Token $command.Token)] = $command
        foreach ($switch in @($command.Switches)) {
            $key = Get-DismOptionKey -Token $switch.Token
            if (-not $specs.ContainsKey($key)) {
                $specs[$key] = $switch
            }
        }
    }
    $script:DismCompletionCatalog.OptionSpecs = $specs
    $script:DismCompletionCatalog.Initialized = $true

    # DISM only prints help from an elevated shell; on any other shell the static catalog is the whole surface.
    if (Test-DismElevated) {
        $topHelp = Invoke-DismHelpText
        if ($topHelp.Count -gt 0) {
            $section = ''
            $liveCommands = New-Object System.Collections.Generic.List[object]
            foreach ($line in $topHelp) {
                if ($line -match '^\s*([A-Z][A-Z\s]+):\s*$') {
                    $section = $matches[1].Trim()
                    continue
                }

                if ($section -like '*COMMANDS*' -and $line -match '^\s*(/[A-Za-z][A-Za-z0-9\-]*:?)\s*(?:-|<|\{)') {
                    $token = $matches[1]
                    $key = Get-DismOptionKey -Token $token
                    if (-not $specs.ContainsKey($key) -and -not $specs.ContainsKey($key + ':')) {
                        $spec = New-DismCommandSpec -Token $token -Description "DISM command $token (from live help)."
                        [void]$liveCommands.Add($spec)
                        $specs[$key] = $spec
                    }
                }
            }

            if ($liveCommands.Count -gt 0) {
                $script:DismCompletionCatalog.Commands = @($static.Commands) + @($liveCommands.ToArray())
            }

            Add-DismLiveSwitchSet -Key '__top__' -HelpLines $topHelp
        }
    }
}

function Update-DismCommandCatalog {
    param([object]$Command, [bool]$Online)

    $key = Get-DismOptionKey -Token $Command.Token
    if ($script:DismCompletionCatalog.LiveCommandKeys.ContainsKey($key)) {
        return
    }
    $script:DismCompletionCatalog.LiveCommandKeys[$key] = $true

    if (-not (Test-DismElevated)) {
        return
    }

    $arguments = @()
    if ($Online) { $arguments += '/Online' }
    $arguments += $Command.Token.TrimEnd(':')
    $help = Invoke-DismHelpText -Arguments $arguments
    if ($help.Count -gt 0) {
        Add-DismLiveSwitchSet -Key $key -HelpLines $help
    }
}

function Get-DismActiveCommand {
    param([string[]]$Tokens)

    foreach ($token in @($Tokens)) {
        $key = Get-DismOptionKey -Token $token
        if ($null -eq $key) {
            continue
        }

        foreach ($command in @($script:DismCompletionCatalog.Commands)) {
            $commandKey = Get-DismOptionKey -Token $command.Token
            if ($commandKey -eq $key -or ($command.Token.EndsWith(':') -and $key -eq $commandKey.TrimEnd(':'))) {
                return $command
            }
        }
    }

    $null
}

function Get-DismOptionSpec {
    param([string]$Key, [object]$ActiveCommand)

    if ($ActiveCommand) {
        foreach ($switch in @($ActiveCommand.Switches)) {
            if ((Get-DismOptionKey -Token $switch.Token) -eq $Key) {
                return $switch
            }
        }
    }

    if ($script:DismCompletionCatalog.OptionSpecs.ContainsKey($Key)) {
        return $script:DismCompletionCatalog.OptionSpecs[$Key]
    }

    $null
}

function New-DismCompletionResult {
    param([string]$CompletionText, [string]$ListItemText, [string]$ResultType, [string]$ToolTip)

    if ([string]::IsNullOrWhiteSpace($ListItemText)) { $ListItemText = $CompletionText }
    if ([string]::IsNullOrWhiteSpace($ToolTip)) { $ToolTip = $CompletionText }
    [System.Management.Automation.CompletionResult]::new($CompletionText, $ListItemText, $ResultType, $ToolTip)
}

function ConvertFrom-DismTypedWord {
    # The value of a typed word and the quote it opened with ('' when bare). A word opened with a quote (ASCII or
    # typographic) is read by the PowerShell tokenizer, which drops the quotes and undoes that quote style's escapes.
    param([string]$Value)

    if ($Value -notmatch '^[''"\u2018-\u201E]') {
        return [pscustomobject]@{ Value = $Value; Quote = '' }
    }

    $tokens = $null
    $parseErrors = $null
    [void][System.Management.Automation.Language.Parser]::ParseInput($Value, [ref]$tokens, [ref]$parseErrors)
    [pscustomobject]@{ Value = [string]$tokens[0].Value; Quote = $Value.Substring(0, 1) }
}

function ConvertTo-DismQuotedValue {
    # Renders a value as one PowerShell argument: bare when safe and no quote was typed, otherwise in the quote the
    # user typed (single by default). PowerShell reads ' and U+2018-U+201B as single quotes and " and U+201C-U+201E
    # as double quotes.
    param([string]$Value, [string]$QuoteChar = '')

    if ([string]::IsNullOrEmpty($Value)) {
        return $Value
    }

    if (-not $QuoteChar) {
        if ($Value -notmatch '[\s{}();,|&<>''"`$@#\u2018-\u201E]' -and $Value -notmatch '^[-\u2013-\u2015]') {
            return $Value
        }

        $QuoteChar = "'"
    }

    if ($QuoteChar -match '^[''\u2018-\u201B]$') {
        return $QuoteChar + ($Value -replace '([''\u2018-\u201B])', '$1$1') + $QuoteChar
    }

    $QuoteChar + ($Value -replace '([`"$\u201C-\u201E])', '`$1') + $QuoteChar
}

function Get-DismPathCompletions {
    param(
        [string]$InputPath,
        [string]$Kind,
        [string[]]$AllowedExtensions
    )

    # Split on the last separator instead of Split-Path: '', 'C:\' and '.\' must list that directory's children
    # and the typed prefix must be preserved verbatim.
    $cleanInput = if ($null -eq $InputPath) { '' } else { $InputPath }
    $separatorIndex = $cleanInput.LastIndexOfAny([char[]]@('\', '/'))
    if ($separatorIndex -ge 0) {
        $parentText = $cleanInput.Substring(0, $separatorIndex + 1)
    } elseif ($cleanInput -match '^[A-Za-z]:') {
        $parentText = $cleanInput.Substring(0, 2)
    } else {
        $parentText = ''
    }
    $leaf = $cleanInput.Substring($parentText.Length)

    $parent = if ([string]::IsNullOrEmpty($parentText)) { '.' } else { $parentText }
    $pattern = [System.Management.Automation.WildcardPattern]::Escape($leaf) + '*'
    $extensions = @($AllowedExtensions | ForEach-Object { $_.ToLowerInvariant() })

    $items = @(Get-ChildItem -LiteralPath $parent -ErrorAction Ignore | Where-Object { $_.Name -like $pattern } | Sort-Object -Property Name)
    foreach ($item in $items) {
        if (-not $item.PSIsContainer) {
            if ($Kind -eq 'Directory') { continue }
            if ($extensions.Count -gt 0 -and ($extensions -notcontains $item.Extension.ToLowerInvariant())) { continue }
        }

        $text = $parentText + $item.Name
        if ($item.PSIsContainer) { $text += [System.IO.Path]::DirectorySeparatorChar }
        [pscustomobject]@{
            Text        = $text
            IsContainer = $item.PSIsContainer
            FullName    = $item.FullName
        }
    }
}

function Get-DismOptionValueCompletion {
    param(
        [string]$OptionPrefix,
        [string]$TypedValue,
        [object]$Spec,
        [string]$TokenQuote
    )

    # A quote typed before the option ('/ImageFile:...) wraps the whole word and one typed after the colon
    # (/ImageFile:'...) wraps only the value; with neither, a value that needs quoting quotes the whole word.
    $valueQuote = ''
    $cleanValue = $TypedValue
    if (-not $TokenQuote) {
        $valueWord = ConvertFrom-DismTypedWord -Value $TypedValue
        $valueQuote = $valueWord.Quote
        $cleanValue = $valueWord.Value
    }
    $formatValue = {
        param([string]$Value)
        if ($valueQuote) { $OptionPrefix + (ConvertTo-DismQuotedValue -Value $Value -QuoteChar $valueQuote) }
        else { ConvertTo-DismQuotedValue -Value ($OptionPrefix + $Value) -QuoteChar $TokenQuote }
    }

    switch ($Spec.Kind) {
        'Enum' {
            foreach ($value in @($Spec.Values)) {
                if ($value.StartsWith($cleanValue, [System.StringComparison]::OrdinalIgnoreCase)) {
                    New-DismCompletionResult -CompletionText (& $formatValue $value) -ListItemText ($OptionPrefix + $value) -ResultType 'ParameterValue' -ToolTip $Spec.Description
                }
            }
            return
        }
        { $_ -in @('Directory', 'File') } {
            $paths = @(Get-DismPathCompletions -InputPath $cleanValue -Kind $Spec.Kind -AllowedExtensions $Spec.Extensions)
            if ($paths.Count -eq 0) {
                if ([string]::IsNullOrEmpty($cleanValue)) {
                    $hint = if ($Spec.Kind -eq 'Directory') { '<directory>' } else { '<path>' }
                    New-DismCompletionResult -CompletionText ($OptionPrefix + $hint) -ResultType 'ParameterValue' -ToolTip $Spec.Description
                }
                return
            }

            foreach ($path in $paths) {
                $resultType = if ($path.IsContainer) { 'ProviderContainer' } else { 'ProviderItem' }
                New-DismCompletionResult -CompletionText (& $formatValue $path.Text) -ListItemText ($OptionPrefix + $path.Text) -ResultType $resultType -ToolTip $path.FullName
            }
            return
        }
        default {
            if ([string]::IsNullOrEmpty($cleanValue)) {
                New-DismCompletionResult -CompletionText ($OptionPrefix + $Spec.Placeholder) -ResultType 'ParameterValue' -ToolTip $Spec.Description
            }
        }
    }
}

function Get-DismSwitchSuggestion {
    param([object]$ActiveCommand, [string]$CurrentWord)

    $suggestions = New-Object System.Collections.Generic.List[object]
    $seen = @{}
    $candidates = New-Object System.Collections.Generic.List[object]

    if ($ActiveCommand) {
        foreach ($switch in @($ActiveCommand.Switches)) { [void]$candidates.Add($switch) }
        $commandKey = Get-DismOptionKey -Token $ActiveCommand.Token
        if ($script:DismCompletionCatalog.ExtraSwitchesByKey.ContainsKey($commandKey)) {
            foreach ($switch in @($script:DismCompletionCatalog.ExtraSwitchesByKey[$commandKey])) { [void]$candidates.Add($switch) }
        }
        foreach ($switch in @($script:DismCompletionCatalog.GlobalSwitches)) { [void]$candidates.Add($switch) }
    } else {
        foreach ($command in @($script:DismCompletionCatalog.Commands)) { [void]$candidates.Add($command) }
        foreach ($switch in @($script:DismCompletionCatalog.GlobalSwitches)) { [void]$candidates.Add($switch) }
        if ($script:DismCompletionCatalog.ExtraSwitchesByKey.ContainsKey('__top__')) {
            foreach ($switch in @($script:DismCompletionCatalog.ExtraSwitchesByKey['__top__'])) { [void]$candidates.Add($switch) }
        }
    }

    foreach ($candidate in $candidates) {
        $key = $candidate.Token.ToLowerInvariant()
        if ($seen.ContainsKey($key)) { continue }
        if (-not [string]::IsNullOrEmpty($CurrentWord) -and -not $candidate.Token.StartsWith($CurrentWord, [System.StringComparison]::OrdinalIgnoreCase)) { continue }
        $seen[$key] = $true
        [void]$suggestions.Add((New-DismCompletionResult -CompletionText $candidate.Token -ResultType 'ParameterName' -ToolTip $candidate.Description))
    }

    @($suggestions.ToArray())
}

function Complete-Dism {
    param(
        [string]$WordToComplete,
        [System.Management.Automation.Language.CommandAst]$CommandAst,
        [int]$CursorPosition
    )

    Initialize-DismCompletionCatalog

    # Tokens that end before the cursor form the context; the element under the cursor is the current word, cut at
    # the cursor; anything to the right is ignored.
    $tokensBefore = New-Object System.Collections.Generic.List[string]
    $currentWord = ''
    foreach ($element in @($CommandAst.CommandElements | Select-Object -Skip 1)) {
        $extent = $element.Extent
        if ($extent.EndOffset -lt $CursorPosition) {
            [void]$tokensBefore.Add($extent.Text)
            continue
        }
        if ($extent.StartOffset -le $CursorPosition) {
            $currentWord = $extent.Text.Substring(0, $CursorPosition - $extent.StartOffset)
        }
        break
    }
    if ([string]::IsNullOrEmpty($currentWord) -and -not [string]::IsNullOrEmpty($WordToComplete)) { $currentWord = $WordToComplete }

    $typedWord = ConvertFrom-DismTypedWord -Value $currentWord
    $cleanWord = $typedWord.Value

    $activeCommand = Get-DismActiveCommand -Tokens @($tokensBefore.ToArray())
    $online = [bool](@($tokensBefore.ToArray()) | Where-Object { (Get-DismOptionKey -Token $_) -eq '/online' })
    if ($activeCommand) {
        Update-DismCommandCatalog -Command $activeCommand -Online $online
    }

    if ($cleanWord -match '^(/[A-Za-z][A-Za-z0-9\-]*:)(.*)$') {
        $optionPrefix = $matches[1]
        $typedValue = $matches[2]
        $spec = Get-DismOptionSpec -Key $optionPrefix.ToLowerInvariant() -ActiveCommand $activeCommand
        if ($spec) {
            return @(Get-DismOptionValueCompletion -OptionPrefix $optionPrefix -TypedValue $typedValue -Spec $spec -TokenQuote $typedWord.Quote)
        }

        return @()
    }

    if ($cleanWord.StartsWith('/') -or [string]::IsNullOrWhiteSpace($cleanWord)) {
        return @(Get-DismSwitchSuggestion -ActiveCommand $activeCommand -CurrentWord $cleanWord)
    }

    if ($cleanWord -like '*\*' -or $cleanWord -like '[A-Za-z]:*') {
        return @(Get-DismPathCompletions -InputPath $cleanWord -Kind 'File' -AllowedExtensions @() | ForEach-Object {
            New-DismCompletionResult -CompletionText (ConvertTo-DismQuotedValue -Value $_.Text -QuoteChar $typedWord.Quote) -ListItemText $_.Text -ResultType $(if ($_.IsContainer) { 'ProviderContainer' } else { 'ProviderItem' }) -ToolTip $_.FullName
        })
    }

    @()
}

Register-ArgumentCompleter -Native -CommandName 'dism' -ScriptBlock {
    param($wordToComplete, $commandAst, $cursorPosition)

    Complete-Dism -WordToComplete $wordToComplete -CommandAst $commandAst -CursorPosition $cursorPosition
}