Skip to content

Instantly share code, notes, and snippets.

@santisq
Last active February 22, 2025 18:52
Show Gist options
  • Save santisq/5050979163e58f7d509d01a601ada761 to your computer and use it in GitHub Desktop.
Save santisq/5050979163e58f7d509d01a601ada761 to your computer and use it in GitHub Desktop.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Management.Automation;
using System.Security;
using System.Text;
using Microsoft.Win32;
namespace PSTree;
[Cmdlet(VerbsCommon.Get, "PSTreeRegistry", DefaultParameterSetName = "Path")]
[OutputType(typeof(TreeRegistryKey), typeof(TreeRegistryValue))]
public sealed class GetPSTreeRegistryCommand : PSCmdlet
{
private string[]? _paths;
private readonly List<TreeRegistryValue> _values = [];
private readonly List<TreeRegistryBase> _result = [];
private readonly Stack<(TreeRegistryKey, RegistryKey)> _stack = [];
private readonly Dictionary<string, RegistryKey> _map = new()
{
["HKEY_CURRENT_USER"] = Registry.CurrentUser,
["HKEY_LOCAL_MACHINE"] = Registry.LocalMachine
};
[Parameter(
ParameterSetName = "Path",
Position = 0,
ValueFromPipeline = true
)]
[SupportsWildcards]
[ValidateNotNullOrEmpty]
public string[]? Path
{
get => _paths;
set => _paths = value;
}
[Parameter(
ParameterSetName = "LiteralPath",
ValueFromPipelineByPropertyName = true
)]
[Alias("PSPath")]
[ValidateNotNullOrEmpty]
public string[]? LiteralPath
{
get => _paths;
set => _paths = value;
}
[Parameter]
[ValidateRange(0, int.MaxValue)]
public int Depth { get; set; } = 3;
[Parameter]
public SwitchParameter Recurse { get; set; }
protected override void BeginProcessing()
{
if (Recurse.IsPresent && !MyInvocation.BoundParameters.ContainsKey("Depth"))
{
Depth = int.MaxValue;
}
}
protected override void ProcessRecord()
{
foreach (string path in EnumerateResolvedPaths())
{
if (!TryGetKey(path, out RegistryKey? key))
{
continue;
}
WriteObject(Traverse(key), enumerateCollection: true);
}
}
private bool TryGetKey(string path, [NotNullWhen(true)] out RegistryKey? key)
{
(string @base, string subkey) = path.Split('\\', 2);
key = default;
if (!_map.TryGetValue(@base, out RegistryKey? value))
{
return false;
}
if (!string.IsNullOrWhiteSpace(subkey))
{
try
{
if ((key = value.OpenSubKey(subkey)) is null)
{
WriteError(path.ToResolvePathError());
return false;
}
}
catch (SecurityException exception)
{
WriteError(exception.ToSecurityError(path));
return false;
}
return true;
}
key = value;
return true;
}
private TreeRegistryBase[] Traverse(RegistryKey key)
{
Clear();
_stack.Push(key.CreateTreeKey(System.IO.Path.GetFileName(key.Name)));
while (_stack.Count > 0)
{
(TreeRegistryKey tree, key) = _stack.Pop();
int depth = tree.Depth + 1;
foreach (string value in key.GetValueNames())
{
if (string.IsNullOrEmpty(value))
{
continue;
}
_values.Add(new TreeRegistryValue(key, value, depth));
}
if (depth <= Depth)
{
PushSubKeys(key, depth);
}
_result.Add(tree);
if (_values.Count > 0)
{
_result.AddRange([.. _values]);
_values.Clear();
}
key.Dispose();
}
return _result.ToArray().Format();
}
private void PushSubKeys(RegistryKey key, int depth)
{
foreach (string keyname in key.GetSubKeyNames())
{
try
{
RegistryKey? subkey = key.OpenSubKey(keyname);
if (subkey is null)
{
continue;
}
_stack.Push(subkey.CreateTreeKey(keyname, depth));
}
catch (Exception exception)
{
WriteError(exception.ToNotSpecifiedError(keyname));
}
}
}
private IEnumerable<string> EnumerateResolvedPaths()
{
Collection<string> resolvedPaths;
ProviderInfo provider;
bool isLiteral = MyInvocation.BoundParameters.ContainsKey(nameof(LiteralPath));
foreach (string path in _paths ?? [SessionState.Path.CurrentLocation.Path])
{
if (isLiteral)
{
string resolvedPath = SessionState.Path.GetUnresolvedProviderPathFromPSPath(
path, out provider, out _);
if (!WriteErrorIfInvalidProvider(provider, resolvedPath))
{
yield return resolvedPath;
}
continue;
}
try
{
resolvedPaths = GetResolvedProviderPathFromPSPath(path, out provider);
}
catch (Exception exception)
{
WriteError(exception.ToResolvePathError(path));
continue;
}
foreach (string resolvedPath in resolvedPaths)
{
if (!WriteErrorIfInvalidProvider(provider, resolvedPath))
{
yield return resolvedPath;
}
}
}
}
private bool WriteErrorIfInvalidProvider(ProviderInfo provider, string path)
{
if (provider.Name == "Registry")
{
return false;
}
WriteError(provider.ToInvalidProviderError(path));
return true;
}
private void Clear()
{
_stack.Clear();
_values.Clear();
_result.Clear();
}
}
public abstract class TreeRegistryBase
{
public string Hierarchy { get; internal set; }
public string Name { get; }
public int Depth;
protected TreeRegistryBase(string hierarchy, string name) =>
(Hierarchy, Name) = (hierarchy, name);
protected static string Combine(RegistryKey key, string value) =>
Path.Combine(key.Name, value);
}
public sealed class TreeRegistryValue : TreeRegistryBase
{
public RegistryValueKind Kind { get; }
internal TreeRegistryValue(RegistryKey key, string value, int depth) :
base(value.Indent(depth), Combine(key, value))
{
Kind = key.GetValueKind(value);
Depth = depth;
}
}
public sealed class TreeRegistryKey : TreeRegistryBase
{
public string Kind { get; } = "RegistryKey";
internal TreeRegistryKey(RegistryKey key, string name, int depth) :
base(name.Indent(depth), key.Name)
{
Depth = depth;
}
internal TreeRegistryKey(RegistryKey key, string name) :
base(name, key.Name)
{ }
}
internal static class Extensions
{
[ThreadStatic]
private static StringBuilder? s_sb;
internal static string Indent(this string inputString, int indentation)
{
s_sb ??= new StringBuilder();
s_sb.Clear();
return s_sb.Append(' ', (4 * indentation) - 4)
.Append("└── ")
.Append(inputString)
.ToString();
}
internal static TreeRegistryBase[] Format(
this TreeRegistryBase[] tree)
{
int index;
for (int i = 0; i < tree.Length; i++)
{
TreeRegistryBase current = tree[i];
if ((index = current.Hierarchy.IndexOf('└')) == -1)
{
continue;
}
for (int z = i - 1; z >= 0; z--)
{
current = tree[z];
string hierarchy = current.Hierarchy;
if (char.IsWhiteSpace(hierarchy[index]))
{
current.Hierarchy = hierarchy.ReplaceAt(index, '│');
continue;
}
if (hierarchy[index] == '└')
{
current.Hierarchy = hierarchy.ReplaceAt(index, '├');
}
break;
}
}
return tree;
}
private static string ReplaceAt(this string input, int index, char newChar)
{
char[] chars = input.ToCharArray();
chars[index] = newChar;
return new string(chars);
}
internal static (TreeRegistryKey, RegistryKey) CreateTreeKey(this RegistryKey key, string name) =>
(new TreeRegistryKey(key, name), key);
internal static (TreeRegistryKey, RegistryKey) CreateTreeKey(this RegistryKey key, string name, int depth) =>
(new TreeRegistryKey(key, name, depth), key);
internal static ErrorRecord ToInvalidProviderError(this ProviderInfo provider, string path) =>
new(new ArgumentException($"The resolved path '{path}' is not a Registry path but '{provider.Name}'."),
"InvalidProvider", ErrorCategory.InvalidArgument, path);
internal static ErrorRecord ToResolvePathError(this Exception exception, string path) =>
new(exception, "ResolvePath", ErrorCategory.NotSpecified, path);
internal static ErrorRecord ToResolvePathError(this string path) =>
new(new ItemNotFoundException(
$"Cannot find path '{path}' because it does not exist."),
"PathNotFound",
ErrorCategory.ObjectNotFound,
path);
internal static ErrorRecord ToNotSpecifiedError(this Exception exception, object? context = null) =>
new(exception, exception.GetType().Name, ErrorCategory.NotSpecified, context);
internal static ErrorRecord ToSecurityError(this SecurityException exception, string path) =>
new(exception, "SecurityException", ErrorCategory.OpenError, path);
internal static void Deconstruct(this string[] strings, out string @base, out string subKey) =>
(@base, subKey) = (strings[0], strings[1]);
}
using namespace System.Collections.Generic
using namespace System.IO
using namespace System.Text
using namespace Microsoft.Win32
class TreeThing {
[string] $Kind
[string] $Hierarchy
hidden [int] $_depth
hidden static [StringBuilder] $s_sb = [StringBuilder]::new()
static [ValueTuple[TreeThing, RegistryKey]] CreateKey([RegistryKey] $key, [int] $depth) {
return [ValueTuple[TreeThing, RegistryKey]]::new(
[TreeThing]@{
Hierarchy = [TreeThing]::Indent([Path]::GetFileName($key.Name), $depth)
Kind = 'Key'
_depth = $depth
}, $key)
}
static [ValueTuple[TreeThing, RegistryKey]] CreateKey([RegistryKey] $key) {
return [ValueTuple[TreeThing, RegistryKey]]::new(
[TreeThing]@{
Hierarchy = [Path]::GetFileName($key.Name)
Kind = 'Key'
_depth = 0
}, $key)
}
static [TreeThing] CreateValue([RegistryKey] $ref, [string] $value, [int] $depth) {
return [TreeThing]@{
Hierarchy = [TreeThing]::Indent($value, $depth)
Kind = $ref.GetValueKind($value)
}
}
static [string] Indent([string] $name, $depth) {
[TreeThing]::s_sb.Clear()
return [TreeThing]::s_sb.Append(' ', (4 * $depth) - 4).
Append('└── ').
Append($name).
ToString()
}
static [TreeThing[]] ToTree([TreeThing[]] $trees) {
for ($i = 0; $i -lt $trees.Length; $i++) {
$current = $trees[$i]
if (($index = $current.Hierarchy.IndexOf('└')) -eq -1) {
continue
}
for ($z = $i - 1; $z -ge 0; $z--) {
$current = $trees[$z]
if (![char]::IsWhiteSpace($current.Hierarchy[$index])) {
[TreeThing]::UpdateCorner($index, $current)
break
}
$replace = $current.Hierarchy.ToCharArray()
$replace[$index] = '│'
$current.Hierarchy = [string]::new($replace)
}
}
return $trees
}
static [void] UpdateCorner([int] $index, [TreeThing] $current) {
if ($current.Hierarchy[$index] -eq '└') {
$replace = $current.Hierarchy.ToCharArray()
$replace[$index] = '├'
$current.Hierarchy = [string]::new($replace)
}
}
}
$reg = Get-Item HKLM:\
$stack = [Stack[ValueTuple[TreeThing, RegistryKey]]]::new()
$stack.Push([TreeThing]::CreateKey($reg))
$maxdepth = 3
$result = while ($stack.Count) {
$current = $stack.Pop()
$tree, $key = $current.Item1, $current.Item2
$depth = $tree._depth + 1
foreach ($value in $key.GetValueNames()) {
[TreeThing]::CreateValue($key, $value, $depth)
}
foreach ($sub in $key.GetSubKeyNames()) {
if ($depth -gt $maxdepth) {
continue
}
try {
$stack.Push([TreeThing]::CreateKey($key.OpenSubKey($sub), $depth))
}
catch {
}
}
$tree
${key}?.Dispose()
}
[TreeThing]::ToTree($result)
@santisq
Copy link
Author

santisq commented Oct 19, 2024

image

@eabase
Copy link

eabase commented Feb 18, 2025

Wow! Looks cool! 💯
But how do you run it?
(Do I need to compile the cs file?)

@santisq
Copy link
Author

santisq commented Feb 18, 2025

Wow! Looks cool! 💯 But how do you run it? (Do I need to compile the cs file?)

Hi @eabase, glad you like it 😄 if you want to use the .cs version it will depend mostly on your PowerShell version, if you use PowerShell latest (7.5) you should be able to just do:

$code = Get-Content path\to\Get-PSTreeRegistry.cs -Raw
Add-Type $code -WA 0 -IgnoreWarnings -PassThru | Import-Module -Assembly { $_.Assembly }

If you wanted to use it in PowerShell 5.1 you will need to actually dotnet publish it using probably netstandard2.0 as your TargetFramework. The code will not be compatible with C# version 5 (what PowerShell 5.1 uses for Add-Type).

@eabase
Copy link

eabase commented Feb 18, 2025

Great, this is perfect. Thank you! I'm using 7.5.0and don't see why anyone would be using anything else.
But I still don't see what the PS1 script is doing, or it's used for? (I don't see it pull in the *.cs script.)

@santisq
Copy link
Author

santisq commented Feb 18, 2025

Great, this is perfect. Thank you! I'm using 7.5.0and don't see why anyone would be using anything else. But I still don't see what the PS1 script is doing, or it's used for? (I don't see it pull in the *.cs script.)

@eabase .ps1 is just the powershell version of the binary cmdlet. you can use either one. the binary cmdlet is more refined iirc.

@santisq
Copy link
Author

santisq commented Feb 22, 2025

@eabase the cmdlet was added to my module in case you want to use it, see https://github.com/santisq/PSTree/releases/tag/v2.2.2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment