Browse Source

fix(items): match -Filter of Get-ChildItem2 with the dot rules of Windows PowerShell 5.1

Get-ChildItem2 -Filter now applies a single matcher, in the cmdlet, that
follows the Win32 "DOS" wildcard rules Windows PowerShell 5.1 applies to
Get-ChildItem -Filter, on the real name of the item, in both editions
(Decision 27, item 4, option B). Before this change the pattern was
matched twice: once by the AlphaFS enumeration and once by the cmdlet
with a WildcardPattern that read a dot as an ordinary character, so the
two editions, and Get-ChildItem itself, disagreed on patterns with a
dot. AlphaFS now always enumerates with "*", and the new matcher decides
alone.

What a user sees changing:
- "Report.*" also returns "Report", which has no dot (previously only
  names with a dot matched a pattern ending in ".*").
- A pattern that ends in a dot, such as "Report." or "Rep*.", now
  returns the names without a dot, instead of nothing.
- "*.*.*" now returns every item, the same as "*.*" and a plain "*",
  instead of only names with two literal dots.
- Breaking: an empty -Filter ('') now returns every item, like no
  filter at all, instead of nothing. This matches the policy of
  Windows PowerShell 5.1 for every other dot pattern; a script that
  relied on an empty -Filter returning nothing should pass -Filter '*'
  explicitly to keep that behavior from before.
- A single dot ('.') as -Filter also returns every item, matching
  Get-ChildItem.
- Get-ChildItem2 deliberately no longer follows PowerShell 7's own
  Get-ChildItem -Filter for a pattern that ends in a dot (which there
  returns nothing) or for "*.?" (which there returns every item): it
  follows the Windows PowerShell 5.1 rules in both editions instead, so
  the two editions of Get-ChildItem2 agree with each other.
- Unaffected: -Filter matching by the real name and not the 8.3 short
  name, brackets and backticks as ordinary characters, case-insensitive
  matching with the invariant culture, the null-Filter error, and every
  other parameter and its interaction with -Filter.

The matcher was derived and checked against a differential oracle of
real Windows PowerShell 5.1 Get-ChildItem -Filter runs: two independent
sets of 8.3-compliant names (22 and 19 names) against 54,654 generated
patterns each (alphabet a, b, c, ., *, ? up to length 6; the remaining
1,333 of the 55,987 generated patterns per set are refused by the .NET
enumeration API itself for ending in ".." and are not part of either
product's -Filter semantics), with zero differences in both sets.

Tests (Tests\ItemCmdlets.Tests.ps1, "Get-ChildItem2" > "Recursion, type
filters, and depth"):
- Changed the dot-rules table (pins the new values for "Report.*",
  "Report.", "Rep*.", "*.", and an empty -Filter) and the dedicated
  "*.*.*" test (now expects every item), both of which pinned the old,
  changed-on-purpose behavior.
- Added three rows to the existing short-name-sensitive test, to pin
  that the new matcher still reads the real name, not the short name,
  for the trailing-dot and "X.*" rule families.
- Added a 36-row truth table (22 fixed 8.3-compliant names, patterns
  from every family the WI calls out) with the literal result of
  Windows PowerShell 5.1 as static, hand-verified data; the test runs
  no Get-ChildItem of its own.
- Added -Recurse with a selective -Filter across three folder levels,
  a dot -Filter with -File and -Directory, an empty -Filter, and a
  -Filter on a -Path that names a file.

Docs\Cmdlets\Get-ChildItem2.md: restated the -Filter parameter
description and the matching note in NOTES with the Windows PowerShell
5.1 policy and the deliberate PowerShell 7 difference, and removed the
sentences that described the dot rules as a limitation or a mismatch
with Get-ChildItem in Windows PowerShell 5.1.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
pull/131/head
Raimund Andree 1 week ago
parent
commit
522fd7fa06
  1. 4
      Docs/Cmdlets/Get-ChildItem2.md
  2. 213
      NTFSSecurity/ItemCmdlets/GetChildItem2.cs
  3. 147
      Tests/ItemCmdlets.Tests.ps1

4
Docs/Cmdlets/Get-ChildItem2.md

@ -142,7 +142,7 @@ Accept wildcard characters: False
### -Filter
Specifies a name pattern that an item must match to be returned. The pattern supports the asterisk and the question mark as wildcard characters, an asterisk for any number of characters and a question mark for exactly one, and the match ignores case. Any other character stands for itself; a bracket is an ordinary character, so `Report[1].txt` returns the file of that name. As for `Get-ChildItem`, a pattern of an asterisk, a dot, and an asterisk returns every item, also an item without a dot in its name. The default value is `*`, which returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder.
Specifies a name pattern that an item must match to be returned. The pattern follows the wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions, on the real name of the item, not its 8.3 short name: an asterisk matches a run of characters, a question mark matches one character that is not a dot (nothing where a dot or the end of the name follows), and a dot also matches nothing where only wildcards or the end of the pattern follow, so that `Report.*` also returns `Report`, which has no dot, and `*.*` returns every item. The match ignores case and is not affected by the culture of the session. Any other character stands for itself, including a bracket, so `Report[1].txt` returns the file of that name. PowerShell 7 deliberately differs here from the built-in `Get-ChildItem`, whose own `-Filter` returns nothing for a pattern that ends in a dot and returns every item for `*.?`; `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead. The default value is `*`, which returns every item, and an empty value also returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder.
```yaml
Type: String
@ -319,7 +319,7 @@ Before 5.0.0, a `-Path` value that points to a file stopped the cmdlet with an `
Before 5.0.0, `-Filter` read a bracket as the start of a character class, so a file with brackets in its name, such as `Report[1].txt`, was not returned for its name, and a pattern of an asterisk, a dot, and an asterisk dropped the items without a dot in their names, most folders among them.
The names are matched twice, by the enumeration of the AlphaFS library and by the cmdlet, which reads a dot as an ordinary character, and their rules for a dot can differ from those of `Get-ChildItem`: a pattern such as `Report.*` does not return the file `Report`, which has no dot, a pattern that ends in a dot returns nothing, and an empty value returns nothing. Only the pattern of an asterisk, a dot, and an asterisk is treated as a single asterisk.
The cmdlet enumerates broadly and matches each name once, with the Win32 "DOS" wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions (Decision 27, item 4): `Report.*` also returns `Report`, which has no dot, a pattern that ends in a dot returns the names without a dot, and an empty value returns every item, like `*`. PowerShell 7 itself differs from Windows PowerShell 5.1 here: its own `Get-ChildItem -Filter` returns nothing for a pattern that ends in a dot and returns every item for `*.?`. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead of the edition it runs in, so this is a deliberate difference from the built-in `Get-ChildItem` in PowerShell 7 for these two pattern shapes.
Before 5.0.0, a `break`, a `continue`, or a `throw` in a later command of the pipeline did not end the cmdlet for an item below the first folder, also when the later command took the error of a folder that the cmdlet cannot read, for example with `2>&1`.

213
NTFSSecurity/ItemCmdlets/GetChildItem2.cs

@ -26,8 +26,6 @@ namespace NTFSSecurity
private bool identifyHardLinks = false;
private int? depth;
WildcardPattern wildcard = null;
System.Reflection.MethodInfo modeMethodInfo = null;
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
@ -146,15 +144,9 @@ namespace NTFSSecurity
paths = new List<string>() { GetCurrentLocation() };
}
// Only * and ? are wildcards, like in the pattern that the enumeration matches; a bracket or a backtick stands
// for itself. Before 5.0.0, [1] was read as a character class, so a file with brackets in its name was not
// returned for its name. The enumeration returns every item for *.* as Windows does, so the comparison does
// too; with the dot as an ordinary character, it would drop the items without a dot, most folders.
// CultureInvariant: like the enumeration (see MoveNextInvariant), the comparison would otherwise treat I and i as
// different letters with tr-TR and drop items that the enumeration returned.
var pattern = filter == "*.*" ? "*" : filter;
wildcard = new WildcardPattern(pattern.Replace("`", "``").Replace("[", "`[").Replace("]", "`]"), WildcardOptions.Compiled | WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant);
// The names are matched by DosWildcard, not by the enumeration (see WriteFileSystem, which always asks
// AlphaFS for "*"), so there is nothing to pre-compile here: DosWildcard reads the raw value of -Filter
// at the time each name is compared.
modeMethodInfo = typeof(FileSystemCodeMembers).GetMethod("Mode");
getFileSystemModeProperty = (bool)((Hashtable)MyInvocation.MyCommand.Module.PrivateData)["GetFileSystemModeProperty"];
@ -211,7 +203,10 @@ namespace NTFSSecurity
IEnumerator files;
using (new InvariantCultureScope())
{
files = di.EnumerateDirectories(filter, global::System.IO.SearchOption.TopDirectoryOnly).GetEnumerator();
// The enumeration applies no pattern of its own; DosWildcard decides in
// WriteFileSystemInfoCollection, on the real name, with the dot rules of Windows
// PowerShell 5.1.
files = di.EnumerateDirectories("*", global::System.IO.SearchOption.TopDirectoryOnly).GetEnumerator();
}
WriteFileSystemInfoCollection(files);
@ -221,7 +216,7 @@ namespace NTFSSecurity
IEnumerator files;
using (new InvariantCultureScope())
{
files = di.EnumerateFiles(filter, global::System.IO.SearchOption.TopDirectoryOnly).GetEnumerator();
files = di.EnumerateFiles("*", global::System.IO.SearchOption.TopDirectoryOnly).GetEnumerator();
}
WriteFileSystemInfoCollection(files);
@ -231,7 +226,7 @@ namespace NTFSSecurity
IEnumerator files;
using (new InvariantCultureScope())
{
files = di.EnumerateFileSystemInfos(filter, global::System.IO.SearchOption.TopDirectoryOnly).GetEnumerator();
files = di.EnumerateFileSystemInfos("*", global::System.IO.SearchOption.TopDirectoryOnly).GetEnumerator();
}
WriteFileSystemInfoCollection(files);
@ -304,7 +299,7 @@ namespace NTFSSecurity
while (MoveNextInvariant(fileSystemInfos))
{
FileSystemInfo current = (FileSystemInfo)fileSystemInfos.Current;
if (!wildcard.IsMatch(current.Name))
if (!DosWildcard.IsMatch(current.Name, filter))
{
continue;
}
@ -401,5 +396,193 @@ namespace NTFSSecurity
global::System.Threading.Thread.CurrentThread.CurrentCulture = previous;
}
}
// Matches a -Filter pattern against the real name of an item with the Win32 "DOS" wildcard rules that
// Windows PowerShell 5.1 applies to Get-ChildItem -Filter, in both editions, so that the two editions and
// the short-name based matching that a past fix removed do not disagree. The rules come from
// FsRtlIsNameInExpression with the Win32 translation of a DOS pattern (DOS_STAR, DOS_QM, DOS_DOT): an
// asterisk matches a run of characters (but not past a dot that a following dot token still has to
// account for); a question mark matches one character that is not a dot, or nothing where the name has
// no more characters left or the next one is a dot; and a dot matches a literal dot in the name, or
// nothing where the name has no more characters left, which is why "Report.*" also returns "Report" and
// "Report." returns only the names without a dot. Comparisons are case-insensitive with the invariant
// culture, never the thread culture, matching the enumeration (see InvariantCultureScope). Every
// character other than an asterisk or a question mark is ordinary, including a bracket and a backtick.
private static class DosWildcard
{
private enum TokenKind
{
Literal,
Star,
QuestionMark,
Dot
}
private struct Token
{
public TokenKind Kind;
public char Char;
}
public static bool IsMatch(string name, string pattern)
{
// An empty value matches every item, like a plain asterisk; Get-ChildItem2 keeps its own default
// of "*" for -Filter, but once a caller passes an explicit empty string this is the policy.
if (pattern.Length == 0)
{
return true;
}
// A single dot is not a meaningful base/extension split, so Get-ChildItem treats it the same as
// no filter at all, in both editions.
if (pattern == ".")
{
return true;
}
var tokens = Tokenize(pattern);
var matched = new Dictionary<long, bool>();
return Match(name, 0, tokens, 0, matched);
}
private static List<Token> Tokenize(string pattern)
{
var tokens = new List<Token>();
// A run of one or more dots that reaches the end of the pattern collapses into a single DOS_DOT:
// "Report..." and "Report." both ask for "Report" with nothing left of the name to account for.
// A dot elsewhere becomes a DOS_DOT only when the single next character is an asterisk or a
// question mark; otherwise it is an ordinary character that requires a literal dot in the name.
var trailingDotsFrom = pattern.Length;
while (trailingDotsFrom > 0 && pattern[trailingDotsFrom - 1] == '.')
{
trailingDotsFrom--;
}
var hasTrailingDotRun = trailingDotsFrom < pattern.Length;
for (var i = 0; i < pattern.Length; i++)
{
if (hasTrailingDotRun && i == trailingDotsFrom)
{
tokens.Add(new Token { Kind = TokenKind.Dot, Char = '.' });
break;
}
var c = pattern[i];
if (c == '*')
{
tokens.Add(new Token { Kind = TokenKind.Star });
}
else if (c == '?')
{
tokens.Add(new Token { Kind = TokenKind.QuestionMark });
}
else if (c == '.')
{
var next = (i + 1 < pattern.Length) ? pattern[i + 1] : '\0';
var qualifies = (i + 1 >= pattern.Length) || next == '*' || next == '?';
tokens.Add(new Token { Kind = qualifies ? TokenKind.Dot : TokenKind.Literal, Char = '.' });
}
else
{
tokens.Add(new Token { Kind = TokenKind.Literal, Char = c });
}
}
return tokens;
}
// Matches the name, starting at name position ni, against the tokens from token position ti. The
// dictionary memoizes (ti, ni) pairs, because a star token tries every name position from ni onward
// and the same (ti, ni) pair can otherwise be re-examined many times over for a pattern with several
// stars.
private static bool Match(string name, int ni, List<Token> tokens, int ti, Dictionary<long, bool> matched)
{
var key = ((long)ti << 32) | (uint)ni;
bool cached;
if (matched.TryGetValue(key, out cached))
{
return cached;
}
var result = MatchCore(name, ni, tokens, ti, matched);
matched[key] = result;
return result;
}
private static bool MatchCore(string name, int ni, List<Token> tokens, int ti, Dictionary<long, bool> matched)
{
if (ti == tokens.Count)
{
return ni == name.Length;
}
var tok = tokens[ti];
if (tok.Kind == TokenKind.Star)
{
// A star that is directly followed by a dot (a DOS_DOT or an ordinary dot) must leave that dot
// for the dot token to account for: it cannot run past the next dot in the name, which is why
// "Rep*." does not return "Report.txt" although a plain "*" could reach the end of the name by
// crossing the dot. A star with anything else after it, including another star, is free to
// cross dots, which is why "**." returns every item and "R*t" returns "Report.txt".
var max = name.Length;
if (ti + 1 < tokens.Count && tokens[ti + 1].Kind == TokenKind.Dot)
{
var nextDot = name.IndexOf('.', ni);
if (nextDot >= 0)
{
max = nextDot;
}
}
for (var k = ni; k <= max; k++)
{
if (Match(name, k, tokens, ti + 1, matched))
{
return true;
}
}
return false;
}
if (ni == name.Length)
{
// A question mark or a dot can match nothing at the end of the name; anything else needs a
// character that is not there.
if (tok.Kind == TokenKind.QuestionMark || tok.Kind == TokenKind.Dot)
{
return Match(name, ni, tokens, ti + 1, matched);
}
return false;
}
if (tok.Kind == TokenKind.QuestionMark)
{
// A question mark does not match a dot; it matches nothing there instead, leaving the dot for
// a later token.
if (name[ni] == '.')
{
return Match(name, ni, tokens, ti + 1, matched);
}
return Match(name, ni + 1, tokens, ti + 1, matched);
}
if (tok.Kind == TokenKind.Dot)
{
if (name[ni] == '.')
{
return Match(name, ni + 1, tokens, ti + 1, matched);
}
return false;
}
if (char.ToUpperInvariant(tok.Char) == char.ToUpperInvariant(name[ni]))
{
return Match(name, ni + 1, tokens, ti + 1, matched);
}
return false;
}
}
}
}

147
Tests/ItemCmdlets.Tests.ps1

@ -122,6 +122,23 @@ Describe 'Get-ChildItem2' {
Assert-TestSandboxPath -Sandbox $sandbox -Path (@($grandchild) + @($paths))
New-Item -ItemType Directory -Path $grandchild -Force | Out-Null
foreach ($path in $paths) { Set-Content -LiteralPath $path -Value 'Tree' }
# The fixture for the -Filter truth table: 18 files and 4 folders, all 8.3-compliant (at most one dot,
# up to 8 characters of base name and 3 of extension, no spaces), so each real name is also its own
# short name and AlphaFS enumerates the same identity that the matcher compares.
$filterTruthTableFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterTruthTable' -Directory
$truthTableFiles = 'a', 'ab', 'abc', 'a.b', 'ab.c', 'abc.de', 'a.bcd', 'abcdefgh', 'abcdefgh.ijk', 'x.y', 'a1.b2', 'bca', 'cab.cd', 'ccc', 'c.c', 'bcabcabc', 'a2.b1', '1cb'
$truthTableDirs = 'bdir', 'bdir.cd', 'fold', 'fo.ld'
foreach ($name in $truthTableFiles) {
$path = Join-Path -Path $filterTruthTableFolder -ChildPath $name
Assert-TestSandboxPath -Sandbox $sandbox -Path $path
Set-Content -LiteralPath $path -Value $name
}
foreach ($name in $truthTableDirs) {
$path = Join-Path -Path $filterTruthTableFolder -ChildPath $name
Assert-TestSandboxPath -Sandbox $sandbox -Path $path
New-Item -ItemType Directory -Path $path | Out-Null
}
}
It 'Should return the exact tree for <Case>' -ForEach @(
@ -146,6 +163,9 @@ Describe 'Get-ChildItem2' {
@{ Filter = '*.htm'; Expected = @('Page.htm') }
@{ Filter = 'Page?.html'; Expected = @('Page2.html') }
@{ Filter = 'PAGE*'; Expected = @('Page.htm', 'Page2.html') }
@{ Filter = 'Page.ht?'; Expected = @('Page.htm') }
@{ Filter = 'Page2.h*'; Expected = @('Page2.html') }
@{ Filter = 'Page2.'; Expected = @() }
) {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterNames' -Directory
foreach ($name in 'Page.htm', 'Page2.html') {
@ -229,9 +249,12 @@ Describe 'Get-ChildItem2' {
($result.Name | Sort-Object) -join ',' | Should -BeExactly 'NoExtension,NoExtensionFolder,Page.htm'
}
# The enumeration returns every item for *.*.*, as Get-ChildItem does. The cmdlet compares each name with the whole
# pattern again, so that the dots of the pattern are characters of the name, as the help says.
It 'Should return only the items that match the whole pattern for -Filter *.*.*' {
# The cmdlet now has one matcher, not two (Decision 27, item 4): every dot in -Filter whose single next
# character is an asterisk or a question mark, or that has nothing after it, is a DOS_DOT, and *.*.* has
# two such dots, so it returns every item, the same as *.* and a plain *. Before this change, the cmdlet
# compared the name with the pattern a second time with the dots read as ordinary characters, so only a
# name with two literal dots matched.
It 'Should return every item for -Filter *.*.*, also the ones with one dot or none' {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterDots' -Directory
foreach ($name in 'Page.htm', 'NoExtension', 'Two.dots.txt') {
$file = Join-Path -Path $folder -ChildPath $name
@ -241,20 +264,20 @@ Describe 'Get-ChildItem2' {
$result = @(Get-ChildItem2 -Path $folder -Filter '*.*.*' -ErrorAction Stop)
($result.Name -join ',') | Should -BeExactly 'Two.dots.txt'
($result.Name | Sort-Object) -join ',' | Should -BeExactly 'NoExtension,Page.htm,Two.dots.txt'
}
# The names are matched twice, by the enumeration and by the cmdlet, and the rules for a dot differ from those of
# Get-ChildItem, where Report.* also returns Report. The documentation lists this as a limitation; these cases pin
# it, so that a change of the rules is a decision. Report* is the control: without a dot in the pattern, the names
# without a dot are returned.
# The names are matched once, by the cmdlet, with the Win32 "DOS" wildcard rules that Windows PowerShell 5.1
# applies to Get-ChildItem -Filter, in both editions (Decision 27, item 4): Report.* also returns Report,
# which has no dot, and a trailing dot asks for the names without a dot. Report* is the control: without a
# dot in the pattern, the names with and without a dot are both returned, as before.
It 'Should return <Outcome> for -Filter "<Filter>"' -ForEach @(
@{ Filter = 'Report.*'; Expected = 'Report.txt'; Outcome = 'only the names with a dot' }
@{ Filter = 'Report.*'; Expected = 'Report,Report.txt'; Outcome = 'the names with and without a dot' }
@{ Filter = 'Report*'; Expected = 'Report,Report.txt'; Outcome = 'the names with and without a dot' }
@{ Filter = 'Report.'; Expected = ''; Outcome = 'nothing' }
@{ Filter = 'Rep*.'; Expected = ''; Outcome = 'nothing' }
@{ Filter = '*.'; Expected = ''; Outcome = 'nothing' }
@{ Filter = ''; Expected = ''; Outcome = 'nothing' }
@{ Filter = 'Report.'; Expected = 'Report'; Outcome = 'only the name without a dot' }
@{ Filter = 'Rep*.'; Expected = 'Report'; Outcome = 'only the name without a dot' }
@{ Filter = '*.'; Expected = 'Other,Report'; Outcome = 'the names without a dot' }
@{ Filter = ''; Expected = 'Other,Report,Report.txt'; Outcome = 'every item, like no filter at all' }
) {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterDotRules' -Directory
foreach ($name in 'Report', 'Report.txt', 'Other') {
@ -268,6 +291,104 @@ Describe 'Get-ChildItem2' {
($result.Name | Sort-Object) -join ',' | Should -BeExactly $Expected
}
# A fixed truth table, pinned with the literal result of Windows PowerShell 5.1's Get-ChildItem on 8.3-compliant
# names (at most one dot, up to 8 characters of base name and 3 of extension, no spaces), whose short name is
# its own real name, so AlphaFS and the cmdlet agree on which name they compare. The expected values are data,
# not the output of a run of Get-ChildItem: this test is its own oracle.
It 'Should match -Filter "<Filter>" the way Windows PowerShell 5.1 does' -ForEach @(
@{ Filter = '*'; Expected = @('1cb', 'a', 'a.b', 'a.bcd', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk', 'bca', 'bcabcabc', 'bdir', 'bdir.cd', 'c.c', 'cab.cd', 'ccc', 'fo.ld', 'fold', 'x.y') }
@{ Filter = '*.*'; Expected = @('1cb', 'a', 'a.b', 'a.bcd', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk', 'bca', 'bcabcabc', 'bdir', 'bdir.cd', 'c.c', 'cab.cd', 'ccc', 'fo.ld', 'fold', 'x.y') }
@{ Filter = '*.'; Expected = @('1cb', 'a', 'ab', 'abc', 'abcdefgh', 'bca', 'bcabcabc', 'bdir', 'ccc', 'fold') }
@{ Filter = 'a.*'; Expected = @('a', 'a.b', 'a.bcd') }
@{ Filter = 'ab.*'; Expected = @('ab', 'ab.c') }
@{ Filter = 'a.'; Expected = @('a') }
@{ Filter = 'ab.'; Expected = @('ab') }
@{ Filter = 'a*.'; Expected = @('a', 'ab', 'abc', 'abcdefgh') }
@{ Filter = 'ab*.'; Expected = @('ab', 'abc', 'abcdefgh') }
@{ Filter = 'c*.'; Expected = @('ccc') }
@{ Filter = '*.b'; Expected = @('a.b') }
@{ Filter = '*.c?'; Expected = @('ab.c', 'bdir.cd', 'c.c', 'cab.cd') }
@{ Filter = '*.b?'; Expected = @('a.b', 'a1.b2', 'a2.b1') }
@{ Filter = '*.?'; Expected = @('1cb', 'a', 'a.b', 'ab', 'ab.c', 'abc', 'abcdefgh', 'bca', 'bcabcabc', 'bdir', 'c.c', 'ccc', 'fold', 'x.y') }
@{ Filter = '*.??'; Expected = @('1cb', 'a', 'a.b', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'bca', 'bcabcabc', 'bdir', 'bdir.cd', 'c.c', 'cab.cd', 'ccc', 'fo.ld', 'fold', 'x.y') }
@{ Filter = '?'; Expected = @('a') }
@{ Filter = '??'; Expected = @('a', 'ab') }
@{ Filter = 'a?'; Expected = @('a', 'ab') }
@{ Filter = 'b?'; Expected = @() }
@{ Filter = 'ab?'; Expected = @('ab', 'abc') }
@{ Filter = 'a?.'; Expected = @('a', 'ab') }
@{ Filter = 'bc?'; Expected = @('bca') }
@{ Filter = '?.?'; Expected = @('a', 'a.b', 'c.c', 'x.y') }
@{ Filter = 'c.?'; Expected = @('c.c') }
@{ Filter = '*a*'; Expected = @('a', 'a.b', 'a.bcd', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk', 'bca', 'bcabcabc', 'cab.cd') }
@{ Filter = '*c*'; Expected = @('1cb', 'a.bcd', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk', 'bca', 'bcabcabc', 'bdir.cd', 'c.c', 'cab.cd', 'ccc') }
@{ Filter = 'a*.*'; Expected = @('a', 'a.b', 'a.bcd', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk') }
@{ Filter = 'bca*.'; Expected = @('bca', 'bcabcabc') }
@{ Filter = '*.*.*'; Expected = @('1cb', 'a', 'a.b', 'a.bcd', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk', 'bca', 'bcabcabc', 'bdir', 'bdir.cd', 'c.c', 'cab.cd', 'ccc', 'fo.ld', 'fold', 'x.y') }
@{ Filter = '.*'; Expected = @() }
@{ Filter = '*..*'; Expected = @() }
@{ Filter = 'abc'; Expected = @('abc') }
@{ Filter = 'cba'; Expected = @() }
@{ Filter = 'a.b.'; Expected = @('a.b') }
@{ Filter = 'ab.c.'; Expected = @('ab.c') }
@{ Filter = '*b.'; Expected = @('1cb', 'a.b', 'ab') }
@{ Filter = 'cab*'; Expected = @('cab.cd') }
@{ Filter = 'a??'; Expected = @('a', 'ab', 'abc') }
@{ Filter = 'c.c'; Expected = @('c.c') }
@{ Filter = 'ca?'; Expected = @() }
) {
$folder = $filterTruthTableFolder
$result = @(Get-ChildItem2 -Path $folder -Filter $Filter -Force -ErrorAction Stop)
($result.Name | Sort-Object) -join ',' | Should -BeExactly (($Expected | Sort-Object) -join ',')
}
It 'Should apply a -Filter at every level of a recursive listing, directories included' {
$recurseFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterRecurse' -Directory
$paths = @(
'A.txt', 'B.log',
'Sub\C.txt', 'Sub\D.log',
'Sub\Sub2\E.txt', 'Sub\Sub2\F.log'
) | ForEach-Object -Process { Join-Path -Path $recurseFolder -ChildPath $_ }
Assert-TestSandboxPath -Sandbox $sandbox -Path $paths
New-Item -ItemType Directory -Path (Join-Path -Path $recurseFolder -ChildPath 'Sub\Sub2') -Force | Out-Null
foreach ($path in $paths) { Set-Content -LiteralPath $path -Value 'Content' }
$result = @(Get-ChildItem2 -Path $recurseFolder -Recurse -Filter '*.txt' -ErrorAction Stop)
($result.Name | Sort-Object) -join ',' | Should -BeExactly 'A.txt,C.txt,E.txt'
}
It 'Should apply a dot -Filter together with -File and with -Directory' {
$folder = $filterTruthTableFolder
$files = @(Get-ChildItem2 -Path $folder -File -Filter '*.' -Force -ErrorAction Stop)
$directories = @(Get-ChildItem2 -Path $folder -Directory -Filter '*.' -Force -ErrorAction Stop)
($files.Name | Sort-Object) -join ',' | Should -BeExactly '1cb,a,ab,abc,abcdefgh,bca,bcabcabc,ccc'
($directories.Name | Sort-Object) -join ',' | Should -BeExactly 'bdir,fold'
}
It 'Should return every item for an empty -Filter, like no filter at all' {
$folder = $filterTruthTableFolder
$result = @(Get-ChildItem2 -Path $folder -Filter '' -Force -ErrorAction Stop)
$result | Should -HaveCount 22
}
It 'Should apply -Filter to a -Path that is a file' {
$file = Join-Path -Path $tree -ChildPath 'Root.txt'
$matching = @(Get-ChildItem2 -Path $file -Filter '*.txt' -ErrorAction Stop)
$other = @(Get-ChildItem2 -Path $file -Filter '*.log' -ErrorAction Stop)
$matching | Should -HaveCount 1
$matching[0].FullName | Should -Be $file
$other | Should -BeNullOrEmpty
}
It 'Should reject a null -Filter' {
{ Get-ChildItem2 -Path $tree -Filter $null -ErrorAction Stop } |
Should -Throw -ErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' -ExpectedMessage "*'Filter'*"

Loading…
Cancel
Save