-
-
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
commented
Oct 19, 2024
Wow! Looks cool! 💯
But how do you run it?
(Do I need to compile the cs file?)
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
).
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.)
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