General Computing
command-line powershell windows-registry conversion
Updated Thu, 14 Jul 2022 22:15:24 GMT

How to convert .reg files to PowerShell set-itemproperty commands automatically?


I am a tinkerer who does many registry hacks and I hate having to click many .reg files one by one; how do I convert .reg files to PowerShell Set-ItemProperty commands automatically?

  • I found a site that does so [Registry to PowerShell Converter], however the output isn't in the format I wanted; I want it to have the exact same syntax as below using Set-ItemProperty/Remove-Item/New-Item and nothing else:
    Windows Registry Editor Version 5.00
    [HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\CurrentVersion\PushNotifications]
    "NoToastApplicationNotification"=dword:00000001
    
    • cmd:
      Reg Add "HKLM\Software\Policies\Microsoft\Windows\CurrentVersion\PushNotifications" /v "NoToastApplicationNotification" /t REG_DWORD /d 1
      
    • powershell:
      Set-ItemProperty -Path "HKLM:\Software\Policies\Microsoft\Windows\CurrentVersion\PushNotifications" -Name "NoToastApplicationNotification" -Type DWord -Value 1
      

  • The command to produce intended result should be:
    "Set-ItemProperty -Path " + $path + "-Name " + $name + "-Value " + $value
    
    • I have created an ASCII table with information found here and uploaded here, managing this [output]:
      $ASCII=import-csv ".\desktop\ascii.csv"
      [array]$AsciiTable=0..255 | foreach-object{
        $Decimal=$ASCII[$_].DEC
        $Hexadecimal=$ASCII[$_].HEX
        $Binary=$ASCII[$_].BIN
        $Octonary=$ASCII[$_].OCT
        $Symbol=$ASCII[$_].Symbol
        $Value=[char]$_
        $Description=$ASCII[$_].Description
        $HTMLName=$ASCII[$_].HTMLName
        $HTMLNumber=$ASCII[$_].HTMLNumber
        [pscustomobject]@{Decimal=$Decimal;Hexadecimal=$Hexadecimal;Binary=$Binary;Octonary=$Octonary;Symbol=$Symbol;Value=$Value;Description=$Description;HTMLName=$HTMLName;HTMLNumber=$HTMLNumber}
      }
      $AsciiTable | Export-csv ".\Desktop\AsciiTable.csv"
      


Currently I have managed this, which is incomplete, but the idea is to loop through file by index, assigning values to variables via regex match, changing type and hivename to ones used in PowerShell:

$registry=get-content $regfile
for ($i=0;$i -lt $registry.count;$i++){
  $line=$registry | select-object -index $i
  if ($line -match '\[' -and '\]') {
    $path=$line -replace '\[|\]'
    switch ($path)
    {
      {$path -match "HKEY_CLASSES_ROOT"}    {$path=$path -replace "HKEY_CLASSES_ROOT","HKCR:"}
      {$path -match "HKEY_CURRENT_USER"}    {$path=$path -replace "HKEY_CURRENT_USER","HKCU:"}
      {$path -match "HKEY_LOCAL_MACHINE"}   {$path=$path -replace "HKEY_LOCAL_MACHINE","HKLM:"}
      {$path -match "HKEY_USERS"}           {$path=$path -replace "HKEY_USERS","HKU:"}
      {$path -match "HKEY_CURRENT_CONFIG"}  {$path=$path -replace "HKEY_CURRENT_CONFIG","HKCC:"}
    }
  }
  else {
    $name=($line | select-string -pattern "`"([^`"=]+)`"").matches.value | select-object -first 1
    switch ($line)
  {
  {$line -match}
}


There are six registry value types [REG_SZ, REG_BINARY, REG_DWORD, REG_QWORD, REG_MULTI_SZ, REG_EXPAND_SZ] and I have seen only one DWORD value type in .reg files, although I managed to make a registry key containing all types:

  • RegEdit:
    enter image description here
  • .reg:
    Windows Registry Editor Version 5.00
    [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\AarSvc]
    "DependOnService"=hex(7):41,00,75,00,64,00,69,00,6f,00,73,00,72,00,76,00,00,00,\
      00,00
    "Description"="@%SystemRoot%\\system32\\AarSvc.dll,-101"
    "DisplayName"="@%SystemRoot%\\system32\\AarSvc.dll,-100"
    "ErrorControl"=dword:00000001
    "FailureActions"=hex:80,51,01,00,00,00,00,00,00,00,00,00,04,00,00,00,14,00,00,\
      00,01,00,00,00,10,27,00,00,01,00,00,00,10,27,00,00,01,00,00,00,10,27,00,00,\
      00,00,00,00,00,00,00,00
    "ImagePath"=hex(2):25,00,53,00,79,00,73,00,74,00,65,00,6d,00,52,00,6f,00,6f,00,\
      74,00,25,00,5c,00,73,00,79,00,73,00,74,00,65,00,6d,00,33,00,32,00,5c,00,73,\
      00,76,00,63,00,68,00,6f,00,73,00,74,00,2e,00,65,00,78,00,65,00,20,00,2d,00,\
      6b,00,20,00,41,00,61,00,72,00,53,00,76,00,63,00,47,00,72,00,6f,00,75,00,70,\
      00,20,00,2d,00,70,00,00,00
    "ObjectName"="NT Authority\\LocalService"
    "RequiredPrivileges"=hex(7):53,00,65,00,49,00,6d,00,70,00,65,00,72,00,73,00,6f,\
      00,6e,00,61,00,74,00,65,00,50,00,72,00,69,00,76,00,69,00,6c,00,65,00,67,00,\
      65,00,00,00,00,00
    "ServiceSidType"=dword:00000001
    "Start"=dword:00000003
    "Type"=dword:00000060
    "UserServiceFlags"=dword:00000003
    "New Value #1"=hex(b):00,00,00,00,00,00,00,00
    [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\AarSvc\Parameters]
    "ServiceDll"=hex(2):25,00,53,00,79,00,73,00,74,00,65,00,6d,00,52,00,6f,00,6f,\
      00,74,00,25,00,5c,00,53,00,79,00,73,00,74,00,65,00,6d,00,33,00,32,00,5c,00,\
      41,00,61,00,72,00,53,00,76,00,63,00,2e,00,64,00,6c,00,6c,00,00,00
    "ServiceDllUnloadOnStop"=dword:00000001
    "ServiceMain"="ServiceMain"
    


How are registry types determined within a .reg, as the intended end-result is a text file/string array/PowerShell script that contains the converted commands?

  • In a .reg, I know values for type REG_DWORD is written as dword, REG_SZ as plain text enclosed in quotes, REG_QWORD as qword (shown here), and have already mapped registry types to their corresponding PowerShell properties:
    REG_SZ         String
    REG_EXPAND_SZ  ExpandString
    REG_MULTI_SZ   MultiString
    REG_BINARY     Binary
    REG_DWORD      DWord
    REG_QWORD      QWord
    
    With the relations inferred from above:
    switch ($line)
    {
      {$line -match '"="'}      {$type="string"}
      {$line -match "dword"}    {$type="dword"}
      {$line -match "qword"}    {$type="qword"}
      {$line -match "hex\(2\)"} {$type="expandstring";break}
      {$line -match "hex\(7\)"} {$type="multistring";break}
      {$line -match "hex\(b\)"} {$type="qword";break}
      {$line -match "hex"}      {$type="binary"}
    }
    


How can I detect and decode the registry hex babbles and are there other ways to write REG_EXPAND_SZ, REG_MULTI_SZ, and REG_BINARY types in a .reg (i.e. as ExpandString, MultiString and Binary respectively)?

  • Script to parse registry expandable string values to plain text:
    function parse-expandstring {
      PARAM (
        [Parameter(ValueFromPipeline=$true, Mandatory=$true)] [System.String]$expandstring
      )
      $AsciiTable=import-csv ".\desktop\AsciiTable.csv"
      [array]$hex=$expandstring -split'[\,\\]' | where {-not ([string]::IsNullOrWhiteSpace($_))} | %{$_.trimstart()}
      $hexadecimal=0..($hex.count-1) | where {$_ % 2 -ne 1} | foreach-object {$hex[$_]}
      $text=@()
      foreach ($hexadecima in $hexadecimal) {
        for ($i=0;$i -le 255;$i++) {
          if ($AsciiTable[$i].hexadecimal -eq $hexadecima) {
            $text+=$AsciiTable[$i].value
          }
        }
      }
      $text=$text -join ""
      $text
    }
    
  • Function to parse REG_QWORD:
    function parse-qword {
      PARAM (
        [Parameter(ValueFromPipeline=$true, Mandatory=$true)] [System.String]$qword
      )
      [array]$qword=$qword -split','
      $qword=for ($i=$qword.count-1;$i -ge 0;$i--) {$qword[$i]}
      $hexvalue=$qword -join ""
      $hexvalue=$hexvalue.trimstart("0")
      $hexvalue
    }
    
  • Function to parse REG_BINARY:
    function parse-binary {
      PARAM (
        [Parameter(ValueFromPipeline=$true, Mandatory=$true)] [System.String]$binary
      )
      [array]$hex=$binary -split'[,\\]' | where {-not ([string]::IsNullOrWhiteSpace($_))} | %{$_.trimstart()}
      $hex=$hex -join ""
        $hex
    }
    
  • Function to parse REG_MULTI_SZ:
    function parse-multistring {
      PARAM (
        [Parameter(ValueFromPipeline=$true, Mandatory=$true)] [System.String]$multistring
        )
      $AsciiTable=import-csv ".\desktop\AsciiTable.csv"
      [array]$hex=$multistring -split'[\,\\]' | where {-not ([string]::IsNullOrWhiteSpace($_))} | %{$_.trimstart()}
      $hexadecimal=0..($hex.count-1) | where {$_ % 2 -ne 1} | foreach-object {$hex[$_]}
      $text=@()
      foreach ($hexadecima in $hexadecimal) {
        for ($i=0;$i -le 255;$i++) {
          if ($AsciiTable[$i].hexadecimal -eq $hexadecima) {
            if ($i -ne 0) {$text+=$AsciiTable[$i].value}
            else {$text+="\0"}
          }
        }
      }
      $text=$text -join ""
      $text
    }
    


The script is almost complete, having already created Remove-Item, New-Item, and Remove-ItemProperty switch conditions; now, the final piece of the puzzle is to write a regex that matches the values. When this is done, I will post it as an answer here.

  • Pseudo code:
    if $line match [ and ]->$line match [-HKEY -> Remove-Item
    else $registry[$i+1] eq ""->New-Item
    elseif $line match "=-" -> Remove-ItemProperty
    
  • I created an ASCII hashtable to use as a dictionary:
    $asciihex=@{}
    0..255 | % {
      $number=$_
      [string]$hex=$number.tostring('x')
      if ($hex.length -eq 1) {$hex='{1}{0}' -f $hex,'0'}
      $char=[char]$number
      $asciihex.add($hex,$char)
      }
    
    • To look for a character at a given codepoint:
      # Change:
        $asciihex.'00'
      # to:
        $asciihex.'ff'
      
    • To look for a character at any codepoint:
      # Don't use $asciihex to print it
        $asciihex.$codepoint
      

To grep values from lines, use -replace $name+$type to get the value.




Solution


Final version:

Function reg2ps1 {
    [CmdLetBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true, Mandatory=$true)]
        [Alias("FullName")]
        [string]$path,
        $Encoding = "utf8"
    )
    Begin {
        $hive = @{
            "HKEY_CLASSES_ROOT" = "HKCR:"
            "HKEY_CURRENT_USER" = "HKCU:"
            "HKEY_LOCAL_MACHINE" = "HKLM:"
            "HKEY_USERS" = "HKU:"
            "HKEY_CURRENT_CONFIG" = "HKCC:"
        }
        [system.boolean]$isfolder=$false
        $addedpath=@()
    }
    Process {
        switch (test-path $path -pathtype container)
        {
            $true {$files=(get-childitem -path $path -recurse -force -file -filter "*.reg").fullname;$isfolder=$true}
            $false {if($path.endswith(".reg")){$files=$path}}
        }
        foreach($File in $Files) {
            $Commands = @()
            [string]$text=$nul
            $FileContent = Get-Content $File | Where-Object {![string]::IsNullOrWhiteSpace($_)} | ForEach-Object { $_.Trim() }
            $joinedlines = @()
            for ($i=0;$i -lt $FileContent.count;$i++){
                if ($FileContent[$i].EndsWith("\")) {
                    $text=$text+($FileContent[$i] -replace "\\").trim()
                } else {
                    $joinedlines+=$text+$FileContent[$i]
                    [string]$text=$nul
                }
            }
            foreach ($joinedline in $joinedlines) {
                if ($joinedline -match '\[' -and $joinedline -match '\]' -and $joinedline -match 'HKEY') {
                    $key=$joinedline -replace '\[|\]'
                    switch ($key.StartsWith("-HKEY"))
                    {
                        $true {
                            $key=$key.substring(1,$key.length-1)
                            $hivename = $key.split('\')[0]
                            $key = "`"" + ($key -replace $hivename,$hive.$hivename) + "`""
                            $Commands += 'Remove-Item -Path {0} -Force -Recurse' -f $key
                        }
                        $false {
                            $hivename = $key.split('\')[0]
                            $key = "`"" + ($key -replace $hivename,$hive.$hivename) + "`""
                            if ($addedpath -notcontains $key) {
                                $Commands += 'New-Item -Path {0} -ErrorAction SilentlyContinue | Out-Null'-f $key
                                $addedpath+=$key
                            }
                        }
                    }
                }
                elseif ($joinedline -match "`"([^`"=]+)`"=") {
                    [System.Boolean]$delete=$false
                    $name=($joinedline | select-string -pattern "`"([^`"=]+)`"").matches.value | select-object -first 1
                    switch ($joinedline)
                    {
                        {$joinedline -match "=-"} {$commands+=$Commands += 'Remove-ItemProperty -Path {0} -Name {1} -Force' -f $key, $Name;$delete=$true}
                        {$joinedline -match '"="'} {
                            $type="string"
                            $value=$joinedline -replace "`"([^`"=]+)`"="
                        }
                        {$joinedline -match "dword"} {
                            $type="dword"
                            $value=$joinedline -replace "`"([^`"=]+)`"=dword:"
                            $value="0x"+$value
                        }
                        {$joinedline -match "qword"} {
                            $type="qword"
                            $value=$joinedline -replace "`"([^`"=]+)`"=qword:"
                            $value="0x"+$value
                        }
                        {$joinedline -match "hex(\([2,7,b]\))?:"} {
                            $value=($joinedline -replace "`"[^`"=]+`"=hex(\([2,7,b]\))?:").split(",")
                            $hextype=($joinedline | select-string -pattern "hex(\([2,7,b]\))?").matches.value
                            switch ($hextype)
                            {
                                {$hextype -eq 'hex(2)' -or $hextype -eq 'hex(7)'} {
                                    $value=for ($i=0;$i -lt $value.count;$i+=2) {
                                        switch ($hextype)
                                        {
                                            'hex(2)' {if ($value[$i] -ne '00') {[string][char][int]('0x'+$value[$i])}}
                                            'hex(7)' {if ($value[$i] -ne '00') {[string][char][int]('0x'+$value[$i])} else {"\0"}}
                                        }
                                    }
                                    $value=$value -join ""
                                    switch ($hextype)
                                    {
                                        'hex(2)' {$type="expandstring"}
                                        'hex(7)' {$type="multistring"}
                                    }
                                }
                                'hex(b)' {
                                    $type="qword"
                                    $value=for ($i=$value.count-1;$i -ge 0;$i--) {$value[$i]}
                                    $value='0x'+($value -join "").trimstart('0')
                                }
                                'hex' {
                                    $type="binary"
                                    $value='0x'+($value -join "")
                                }
                            }
                        }
                    }
                    if ($delete -eq $false) {$commands+='Set-ItemProperty -Path {0} -Name {1} -Type {2} -Value {3}' -f $key, $name, $type, $value}
                }
                elseif ($joinedline -match "@=") {
                    $name='"(Default)"';$type='string';$value=$joinedline -replace '@='
                    $commands+='Set-ItemProperty -Path {0} -Name {1} -Type {2} -Value {3}' -f $key, $name, $type, $value
                }
            
            }
            $parent=split-path $file -parent
            $filename=[System.IO.Path]::GetFileNameWithoutExtension($file)
            $Commands | out-file -path "${parent}\${filename}_reg.ps1" -encoding $encoding
        }
        if ($isfolder -eq $true) {
            $allcommands=(get-childitem -path $path -recurse -force -file -filter "*_reg.ps1").fullname | where-object {$_ -notmatch "allcommands_reg"} | foreach-object {get-content $_}
            $allcommands | out-file -path "${path}\allcommands_reg.ps1" -encoding $encoding
        }
    }
}
$path = Read-Host "input path"
reg2ps1 $path

This is the final version, based on my previous script and the script provided by SimonS. The script is truly complete, all bugs fixed, it can correctly parse all 6 registry value types: REG_SZ, REG_DWORD, REG_QWORD, REG_BINARY, REG_MULTI_SZ and REG_EXPAND_SZ, converts each [HKEY_* line to a New-Item line, each [-HKEY_* to a Remove-Item line, each "([^"=]+)"=- line to a Remove-ItemProperty line, and each "([^"=]+)"= line to an appropriate Set-ItemProperty line based on the property type. It accepts an inputted path, automatically detect whether the path points to a file or a folder, if a file with extension of .reg, it outputs converted commands to a file at the parent folder of the file with ${filename}_reg.ps1 as its filename; If a folder, converts all .reg files inside that folder, and outputs a ${filename}_reg.ps1 file for each .reg file to that folder, and then put all _reg.ps1 commands into one allcommands.ps1 in the folder.

I made numerous tests and have confirmed it is really really working. The script is now complete. I made major improvements, used better formats, and simplified the code greatly, used better logics and made many other enhancements.

This is truly complete, to use my final version, copy paste the function into an opened powershell window, and invoke it like reg2ps1 "full\path\to\content" or save it as a .ps1 file and run it by cd $scriptdir and .\reg2ps1.ps1, then input full\path\to\content, notice you shouldn't use any quotes, or the path can't be found...


Update

I made a mistake in the code, by specifying the -Force parameter when using New-Item, if the item already exists, it will re-create the item, emptying the item in the process, this is not what I intended, now it's fixed. By removing -Force parameter in the New-Item line, trying to create an item that already exists will generate an error that tells the item exists, and will not reset the item. The error message is hidden by -ErrorAction SilentlyContinue. If the item doesn't exist, it will be created, the item will be empty, the process will prompt a message that tells the item is created, the message is hidden by | Out-Null.