Last active
February 22, 2025 18:52
-
-
Save santisq/5050979163e58f7d509d01a601ada761 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Great, this is perfect. Thank you! I'm using
7.5.0
and 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.
@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
Great, this is perfect. Thank you! I'm using
7.5.0
and 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.)