So, kurzer und ganz flinker Test, der aussagekräftig sein könnte oder vielleicht auch nicht.
1. Wie angedeutet wurde das cmdlet überarbeitet und eine ein wenig optimierte LINQ-Variante überführt. Das Teil ist alles andere als optimal - PLINQ wird nicht verwendet -- aber es soll auch erstmal nur der Veranschaulichung bzw der Überprüfbarkeit dienen. Schließlich hab ich hier nicht sehr breite Testmöglichkeiten.
2. Zur Zeitmessung wurde
PowerShell:
$tsStart = [datetime]::now
$payload = Get-FileSystemEntry -Path $ENV:USERPROFILE -Recurse
$tsEnd = [datetime]::now
$tsDelta = $tsEnd - $tsStart
gesagt.
Ergebnis:
$tsDelta => Dauer: 11.75 Sekunden
$payload.count = 305'507
Zur Gegenüberstellung wurde die Zuweisung $payload= für einen weiteren Test ausgelassen und das Delta ermittelt. Ergebnis:
$tsDelta => Dauer: 4 Minuten 38 Sekunden (4'37"985).
Das ist 24mal so lange und führt hoffentlich
eindringlich vor Augen, wie teuer die Ausgabe ist. Je mehr man davon hat, desto mehr sollte man davon weglassen bzw. irgendwo wegschreiben, zB eben in eine Variable, wenn man damit weiterarbeiten will.
Implementierung:
Ich hab noch ein paar bells+whistles drangebammelt, kann man machen, muß man aber natürlich nicht, nur ein Indiz darauf, was geht.
Listen aller Art sollten readonly sein, wenn sie Parameter sind. Zuweisung zur Laufzeit muß aber möglich sein. Deshalb sind Listen get; protected set; .
LINQ wurde ein bißchen verschachtelter. Hey, es ist LINQ.
Funktionsparameter (in) sind readonly. Keine Ausnahme. Habe deshalb CurrentDepth++ aufgelöst.
Den Rückgabewert kann man weiter filtern, mit LINQ in C#, ggfs mit Hilfe von PLINQ, oder halt auf irgendeine Art in PowerShell.
Die meiste Performance kriegt man, wenn man - wie dargelegt -- auf Ausgaben nach Möglichkeit verzichtet und nur exakt das aufschreiben läßt, was man auch wirklich haben will. Das kann bei PS unter Umständen auch ein Objekt sein - das muß man nur referenzieren und nicht kopieren.
Note: besondere Funktionstests wurden nicht durchgeführt, das Ganze läuft sauber von vorn bis hinten durch, Ausnahmen terminieren nicht. Ich hab sie aber auch nicht gezählt. 😊 Gut möglich, daß es immer noch zwei Meldungen pro Ausnahme sind. Diese können aber, wie angedacht, per -ErrorAction SilentlyContinue ignoriert oder per ... Stop abgefangen werden.
Keine Gewähr, und so weiter. Pures proof-of-concept zur Veranschaulichung. Ob man ohne SearchOption.TopDirectoryOnly vielleicht performanter wegkommt hab ich auch nicht getestet. Das wäre womöglich der trivialste, aber dennoch performantere Ansatz.
C#:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Management.Automation;
namespace DPXone.FileSystem
{
[Cmdlet(VerbsCommon.Get, "FileSystemEntry")]
[CmdletBinding(PositionalBinding = false, ConfirmImpact = ConfirmImpact.None)]
[OutputType(typeof(IEnumerable<string>))]
public class GetFileSystemEntryCmdlet : Cmdlet
{
[Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)]
public List<string> Path { get; internal set; }
[Parameter()]
public SwitchParameter Recurse { get; internal set; }
[Parameter]
[ValidateRange(0, uint.MaxValue)]
public uint Depth { get; internal set; } = 0;
[Parameter]
[SupportsWildcards()]
public string Filter { get; internal set; } = "*";
[Parameter()]
public FSItemType ItemType { get; internal set; } = FSItemType.FileSystemEntry;
protected override void BeginProcessing()
{
base.BeginProcessing();
if (Depth == 0 && Recurse)
{
Depth = uint.MaxValue;
}
}
protected override void ProcessRecord() =>
WriteObject(
sendToPipeline: Path
.SelectMany(PathItem => Search(searchPath: PathItem, searchPattern: Filter, itemType: ItemType, currentDepth: 0, maxDepth: Depth)),
enumerateCollection: true
);
private IEnumerable<string> Search(string searchPath, string searchPattern, FSItemType itemType, uint currentDepth, uint maxDepth)
=> GetFileSystemEntries(searchPath, searchPattern, itemType)
.Concat(
GetFileSystemEntries(searchPath, "*", FSItemType.Directory)
.Where(x => currentDepth + 1 <= maxDepth)
.SelectMany(directory => Search(directory, searchPattern, itemType, currentDepth + 1, maxDepth))
);
private IEnumerable<string> GetFileSystemEntries(string path, string searchPattern, FSItemType itemType)
{
IEnumerable<string> fileSystemEntries = Array.Empty<string>();
//bool hasAccessError = false;
try
{
switch (itemType)
{
case FSItemType.FileSystemEntry:
fileSystemEntries = Directory.EnumerateFileSystemEntries(path, searchPattern, SearchOption.TopDirectoryOnly);
break;
case FSItemType.File:
fileSystemEntries = Directory.EnumerateFiles(path, searchPattern, SearchOption.TopDirectoryOnly);
break;
case FSItemType.Directory:
fileSystemEntries = Directory.EnumerateDirectories(path, searchPattern, SearchOption.TopDirectoryOnly);
break;
}
}
catch (UnauthorizedAccessException ex)
{
WriteError(errorRecord: new ErrorRecord(
exception: ex,
errorId: "FSItemUnauthorizedAccessError",
errorCategory: ErrorCategory.PermissionDenied,
targetObject: path));
}
catch (Exception ex)
{
WriteError(errorRecord: new ErrorRecord(
exception: ex,
errorId: "NotSpecified",
errorCategory: ErrorCategory.NotSpecified,
targetObject: path));
}
return fileSystemEntries;
}
public enum FSItemType
{
FileSystemEntry,
File,
Directory
}
}
}