-
-
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.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.)
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*.csscript.)
@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