Getting BCD entries with .NET (PowerShell or .NET)

2.4k Views Asked by At

I'm creating an application that analyzes the entries in the Boot Configuration Data (BCD).

I've tried with PowerShell, but it seems that it doesn't provide any cmdlets to deal with it. So, I've fallen back to .NET, espically C#.

I would like to have something obtain the BCD entries like this

var entries = bcd.GetEntries();

with entries being an IList<BcdEntry>

class BcdEntry
{
    public string Name {get; set; }
    IDictionary<string, IList<string>> Properties { get; set; }
}

The problem is that I don't know how to obtain the entries. Invoking BCDEdit is a possibility, but it requires to parse the output of the command, that is a tedious task.

I hope you can think of a solution for my problem.

3

There are 3 best solutions below

4
On BEST ANSWER

A PSv4+ solution that parses bcdedit.exe /enum output into a list of custom objects:

# IMPORTANT: bcdedit /enum requires an ELEVATED session.
$bcdOutput = (bcdedit /enum) -join "`n" # collect bcdedit's output as a *single* string

# Initialize the output list.
$entries = New-Object System.Collections.Generic.List[pscustomobject]]

# Parse bcdedit's output.
($bcdOutput -split '(?m)^(.+\n-)-+\n' -ne '').ForEach({
  if ($_.EndsWith("`n-")) { # entry header 
    $entries.Add([pscustomobject] @{ Name = ($_ -split '\n')[0]; Properties = [ordered] @{} })
  } else {  # block of property-value lines
    ($_ -split '\n' -ne '').ForEach({
      $propAndVal = $_ -split '\s+', 2 # split line into property name and value
      if ($propAndVal[0] -ne '') { # [start of] new property; initialize list of values
        $currProp = $propAndVal[0]
        $entries[-1].Properties[$currProp] = New-Object Collections.Generic.List[string]
      }
      $entries[-1].Properties[$currProp].Add($propAndVal[1]) # add the value
    })
  }
})

# Output a quick visualization of the resulting list via Format-Custom
$entries | Format-Custom

Note:

  • As LotPing observes,

    • bcdedit.exe output is partially localized; specifically, the following items:
      • entry headers (e.g., English Windows Boot Manager is Administrador de arranque de Windows in Spanish)
      • curiously, also the name of the property named identifier in English (e.g., Identificador in Spanish).
    • For the sake of brevity, the code makes no attempt to map localized names to their US-English counterparts, but it could be done.

    • Also, the sample bcdedit output posted with this ServerFault question (a duplicate) suggests that there may be property names that are so long that they run into their values, without intervening whitespace and without truncation.
      If that is not just an artifact of posting, more work would be needed to handle this case; this article contains a list of property names.

  • [pscustomobject] instances are used rather than instances of a custom BcdEntry class; in PSv5+, you could create such a custom class directly in PowerShell.

  • The property values are all captured as string values, collected in a [List[string]] list (even if there's only 1 value); additional work would be required to interpret them as specific types;
    e.g., [int] $entries[1].Properties['allowedinmemorysettings'][0] to convert string '0x15000075' to an integer.


Sample input / output:

Given bcdedit.exe /enum output such as this...

Windows Boot Manager
--------------------
identifier              {bootmgr}
device                  partition=C:
displayorder            {current}
                        {e37fc869-68b0-11e8-b4cf-806e6f6e6963}
description             Windows Boot Manager
locale                  en-US
inherit                 {globalsettings}
default                 {current}
resumeobject            {9f3d8468-592f-11e8-a07d-e91e7e2fad8b}
toolsdisplayorder       {memdiag}
timeout                 0

Windows Boot Loader
-------------------
identifier              {current}
device                  partition=C:
path                    \WINDOWS\system32\winload.exe
description             Windows 10
locale                  en-US
inherit                 {bootloadersettings}
recoverysequence        {53f531de-590e-11e8-b758-8854872f7fe5}
displaymessageoverride  Recovery
recoveryenabled         Yes
allowedinmemorysettings 0x15000075
osdevice                partition=C:
systemroot              \WINDOWS
resumeobject            {9f3d8468-592f-11e8-a07d-e91e7e2fad8b}
nx                      OptIn
bootmenupolicy          Standard

... the above command yields this:

class PSCustomObject
{
  Name = Windows Boot Manager
  Properties = 
    [
      class DictionaryEntry
      {
        Key = identifier
        Value = 
          [
            {bootmgr}
          ]

        Name = identifier
      }
      class DictionaryEntry
      {
        Key = device
        Value = 
          [
            partition=C:
          ]

        Name = device
      }
      class DictionaryEntry
      {
        Key = displayorder
        Value = 
          [
            {current}
            {e37fc869-68b0-11e8-b4cf-806e6f6e6963}
          ]

        Name = displayorder
      }
      class DictionaryEntry
      {
        Key = description
        Value = 
          [
            Windows Boot Manager
          ]

        Name = description
      }
      ...
    ]

}

class PSCustomObject
{
  Name = Windows Boot Loader
  Properties = 
    [
      class DictionaryEntry
      {
        Key = identifier
        Value = 
          [
            {current}
          ]

        Name = identifier
      }
      class DictionaryEntry
      {
        Key = device
        Value = 
          [
            partition=C:
          ]

        Name = device
      }
      class DictionaryEntry
      {
        Key = path
        Value = 
          [
            \WINDOWS\system32\winload.exe
          ]

        Name = path
      }
      class DictionaryEntry
      {
        Key = description
        Value = 
          [
            Windows 10
          ]

        Name = description
      }
      ...
    ]

}

To process the entries programmatically:

foreach($entry in $entries) { 
  # Get the name.
  $name = $entry.Name
  # Get a specific property's value.
  $prop = 'device'
  $val = $entry.Properties[$prop] # $val is a *list*; e.g., use $val[0] to get the 1st item
}

Note: $entries | ForEach-Object { <# work with entry $_ #> }, i.e. using the pipeline is an option too, but if the list of entries is already in memory, a foreach loop is faster.

2
On

I made some changes to @mklement0 script, too much to put in comments.

  • To solve the multiline properties problem these properties (which all seem to be enclosed in curly braces) are joined with a RegEx replace.
  • to be locale independent the script uses just the dash line marking the section header, to split contents (one caveat it inserts a blank first entry)
  • I was wondering why there were only 4 Dictionary entries in the output until I found the default value for $FormatEnumerationLimit is 4

  • To avoid line breaks in output the script uses Out-String -Width 4096


## Q:\Test\2018\06\20\SO_50946956.ps1
# IMPORTANT: bcdedit /enu, requires an ELEVATED session.
#requires -RunAsAdministrator

## the following line imports the file posted by SupenJMN for testing
$bcdOutput = (gc ".\BCDEdit_ES.txt") -join "`n" -replace '\}\n\s+\{','},{'
## for a live "bcdedit /enum all" uncomment the following line
# $bcdOutput = (bcdedit /enum all) -join "`n" -replace '\}\n\s+\{','},{'

# Create the output list.
$entries = New-Object System.Collections.Generic.List[pscustomobject]]

# Parse bcdedit's output into entry blocks and construct a hashtable of
# property-value pairs for each.
($bcdOutput -split '(?m)^([a-z].+)\n-{10,100}\n').ForEach({
  if ($_ -notmatch '  +') {
    $entries.Add([pscustomobject] @{ Name = $_; Properties = [ordered] @{} })
  } else {
    ($_ -split '\n' -ne '').ForEach({
      $keyValue = $_ -split '\s+', 2
      $entries[-1].Properties[$keyValue[0]] = $keyValue[1]
    })
  }
})

# Output a quick visualization of the resulting list via Format-Custom
$FormatEnumerationLimit = 20
$entries | Format-Custom | Out-String -Width 4096 | Set-Content BCDEdit_ES_Prop.txt

Shorted sample output of the script (~700 lines)

class PSCustomObject
{
  Name = 
  Properties = 
    [
    ]

}

class PSCustomObject
{
  Name = Administrador de arranque de firmware
  Properties = 
    [
      class DictionaryEntry
      {
        Key = Identificador
        Value = {fwbootmgr}
        Name = Identificador
      }
      class DictionaryEntry
      {
        Key = displayorder
        Value = {bootmgr},{e37fc869-68b0-11e8-b4cf-806e6f6e6963},{05d4f193-712c-11e8-b4ea-806e6f6e6963},{05d4f194-712c-11e8-b4ea-806e6f6e6963},{cb6d5609-712f-11e8-b4eb-806e6f6e6963},{cb6d560a-712f-11e8-b4eb-806e6f6e6963},{cb6d560b-712f-11e8-b4eb-806e6f6e6963}
        Name = displayorder
      }
      class DictionaryEntry
      {
        Key = timeout
        Value = 1
        Name = timeout
      }
    ]

}
0
On

My approach would look somewhat like this:

(bcdedit /enum | Out-String) -split '(?<=\r\n)\r\n' | ForEach-Object {
    $name, $data = $_ -split '\r\n---+\r\n'

    $props = [ordered]@{
        'name' = $name.Trim()
    }

    $data | Select-String '(?m)^(\S+)\s\s+(.*)' -AllMatches |
        Select-Object -Expand Matches |
        ForEach-Object { $props[$_.Groups[1].Value] = $_.Groups[2].Value.Trim() }

    [PSCustomObject]$props
}

The above code basically starts with merging the bcdedit output into a single string like the other answers do, then splits that string into blocks of boot configuration data. Each of these blocks is then split again to separate the title from the actual data. The title is added to a hashtable as the name of the boot config section, then the data block is parsed with a regular expression for key/value pairs. These are appended to the hashtable, which is finally converted to a custom object.

Because of the the ordered and PSCustomObject type accelerators the code requires at least PowerShell v3.

Of course there are various optimizations you could apply to the basic example code above. For instance, different boot config sections might have different properties. The boot manager section has properties like toolsdisplayorder and timeout that are not present in the boot loader section, and the boot loader section has properties like osdevice and systemroot that are not present in the boot manager section. If you want a consistent set of properties for all generated objects you could pipe them through a Select-Object with a list of the properties you want your objects to have, e.g.:

... | Select-Object 'name', 'identifier', 'default', 'osdevice' 'systemroot'

Properties not present in the list will be dropped from the objects, while properties that are not present in an object will be added with an empty value.

Also, instead of creating all values as strings you could convert them to a more fitting type or just modify the value, e.g. to remove curly brackets from a string.

... | ForEach-Object {
    $key = $_.Groups[1].Value
    $val = $_.Groups[2].Value.Trim()

    $val = $val -replace '^\{(.*)\}$', '$1'
    if ($val -match '^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$') {
        $val = [guid]$val
    } elseif ($val -eq 'yes' -or $val -eq 'true') {
        $val = $true
    } elseif ($val -eq 'no' -or $val -eq 'false') {
        $val = $false
    } elseif ($key -eq 'locale') {
        $val = [Globalization.CultureInfo]$val
    }

    $props[$key] = $val
}