Scripts/Sort-RegistryExport.ps1

<#
    .SYNOPSIS
    Lexically sorts the exported values for each registry key in a Windows Registry export
 
    .DESCRIPTION
    Exporting a registry key to a ".reg" file will typically not result in the exported values being lexically sorted.
 
    This command sorts the exported values under each registry key in a Windows Registry export in lexicographical order.
 
    .PARAMETER Path
    The path to a Windows Registry export which will have the values for each exported registry key sorted lexically.
 
    .EXAMPLE
    Sort-Registryexport -Path Export.reg
 
    Sorts the registry values lexically for each exported registry key in the Export.reg file.
 
    .NOTES
    Windows Registry export are expected to have a first line beginning with "Windows Registry Editor".
 
    Registry keys are not sorted (only their values), however, Windows built-in tools export keys in lexical order.
 
    .LINK
    https://github.com/ralish/PSWinGlue
#>


#Requires -Version 3.0

[CmdletBinding()]
[OutputType([Void])]
Param(
    [Parameter(Mandatory)]
    [String]$Path
)

try {
    $RegFile = Get-Item -Path $Path -ErrorAction Stop
} catch {
    throw $_
}

if ($RegFile -isnot [IO.FileInfo]) {
    throw 'Expected a file but received: {0}' -f $RegFile.GetType().Name
}

# Expected registry export file header
$FileHeader = 'Windows Registry Editor Version 5.00'
$RegexFileHeader = '^{0}$' -f [Regex]::Escape($FileHeader)

$RegFileHeader = Get-Content -Path $RegFile -TotalCount 1
if ($RegFileHeader -notmatch $RegexFileHeader) {
    throw 'File does not begin with expected "{0}" header: {1}' -f $FileHeader, $RegFile.Name
}

# Comment line
$RegexComment = '^\s*(;.*)\s*$'
# Registry section
#
# Registry key names can include square brackets. They're not escaped, but
# there's no other content apart from the enclosing square brackets.
$RegexSection = '^\s*(\[.+\])\s*$'
# Registry value
#
# Registry values are always quoted, except for the unnamed (default) value,
# which if set is denoted by an "@" symbol.
$RegexValue = '^\s*[@"]'
# Named registry value
#
# Registry value names can include double quotes and backslashes, both of which
# are escaped. The match group excludes the enclosing double quotes.
$RegexNamedValue = '^\s*"((?:[^"\\]|\\.)+)"'
# Multi-line registry value data
#
# Some registry value data may span multiple lines (e.g. REG_BINARY values),
# which is denoted by a single trailing backslash.
$RegexMultilineValue = '\\\s*$'

$FileContent = Get-Content -Path $Path -ErrorAction Stop
$SortedContent = New-Object -TypeName 'Collections.Generic.List[String]'

$SectionName = [String]::Empty
$SectionContent = New-Object -TypeName 'Collections.Generic.Dictionary[String, String]'

$RegComment = New-Object -TypeName 'Collections.Generic.List[String]'
$RegValue = New-Object -TypeName 'Collections.Generic.List[String]'

# Add the registry file header
$SortedContent.Add($RegFileHeader)

# Add any comments preceding the first section
for ($Idx = 1; $Idx -lt $FileContent.Count; $Idx++) {
    $Line = $FileContent[$Idx]

    if ($Line -notmatch $RegexSection) {
        $SortedContent.Add($Line)
        continue
    }

    $FirstSectionIdx = $Idx
    break
}

# Process each registry section
for ($Idx = $FirstSectionIdx; $Idx -lt $FileContent.Count; $Idx++) {
    $Line = $FileContent[$Idx]

    # Skip blank lines
    if ([String]::IsNullOrWhiteSpace($Line)) { continue }

    # Registry comment
    if ($Line -match $RegexComment) {
        if ($RegComment.Count -gt 0) {
            $RegComment += '{0}{1}' -f [Environment]::NewLine, $Matches[1]
        } else {
            $RegComment = $Matches[1]
        }

        continue
    }

    # Registry section (key path)
    if ($Line -match $RegexSection) {
        $SectionName = $Matches[1]

        # Add any comments preceding the section
        if ($RegComment) {
            $SortedContent.Add($RegComment)
            $RegComment = [String]::Empty
        }

        # Add registry values for the section
        foreach ($ValueName in ($SectionContent.Keys | Sort-Object)) {
            $SortedContent.Add($SectionContent[$ValueName])
        }
        $SectionContent.Clear()

        if ($Idx -ne $FirstSectionIdx) {
            $SortedContent.Add([String]::Empty)
        }

        # Add the start of the new section
        $SortedContent.Add($SectionName)
        continue
    }

    # Registry value (name, type, data)
    if ($Line -match $RegexValue) {
        if ($Line -match $RegexNamedValue) {
            $ValueName = $Matches[1]
        } else {
            $ValueName = '@'
        }

        $RegValue = $Line

        # Add any comments preceding the value
        if ($RegComment) {
            $RegValue = '{0}{1}{2}' -f $RegComment, [Environment]::NewLine, $RegValue
            $RegComment = [String]::Empty
        }

        # Is the value data multi-line?
        if ($Line -match $RegexMultilineValue) {
            do {
                $Idx++
                $ExtraLine = $FileContent[$Idx]
                $RegValue += '{0}{1}' -f [Environment]::NewLine, $ExtraLine
            } while ($ExtraLine -match '\\\s*$')
        }

        $SectionContent.Add($ValueName, $RegValue)
        continue
    }

    throw 'Unexpected content on line {0}: {1}' -f ($Idx + 1), $Line
}

# Add registry values for the final section
foreach ($ValueName in ($SectionContent.Keys | Sort-Object)) {
    $SortedContent.Add($SectionContent[$ValueName])
}

# Add any trailing comments
if ($RegComment) {
    $SortedContent.Add($RegComment)
    $RegComment = [String]::Empty
}

# Registry exports are UTF16-LE encoded
Set-Content -Path $RegFile -Value $SortedContent -Encoding 'unicode'