From 235ff058e71d6caca57d1f06ed49de5fe4cdb223 Mon Sep 17 00:00:00 2001 From: Yoshifumi Date: Thu, 11 Dec 2025 01:22:36 +0900 Subject: [PATCH 1/2] Centralize ExcludeProperty filter application in ViewGenerator base class (#26574) --- .../common/FormatViewGenerator.cs | 46 +++++-- .../common/FormatViewGenerator_Complex.cs | 10 +- .../common/FormatViewGenerator_List.cs | 34 ++--- .../common/FormatViewGenerator_Table.cs | 116 +++++++++--------- .../common/FormatViewGenerator_Wide.cs | 82 ++++++------- 5 files changed, 146 insertions(+), 142 deletions(-) diff --git a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator.cs b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator.cs index cb20e708cc4..963b5a0f88b 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator.cs @@ -348,19 +348,49 @@ protected class DataBaseInfo protected DataBaseInfo dataBaseInfo = new DataBaseInfo(); - protected List activeAssociationList = null; /// - /// Apply ExcludeProperty filter to activeAssociationList if specified. - /// This method filters and updates "activeAssociationList" instance property. + /// Builds the raw association list for the given object. + /// Subclasses override this to provide cmdlet-specific property expansion logic. /// - protected void ApplyExcludePropertyFilter() + /// The object to build the association list for. + /// The list of properties specified by the user, or null if not specified. + /// The raw association list, or null if not applicable. + protected virtual List BuildRawAssociationList(PSObject so, List propertyList) { - if (this.parameters is not null && this.parameters.excludePropertyFilter is not null) + return null; + } + + /// + /// Builds the active association list for the given object, with ExcludeProperty filter applied. + /// + /// The object to build the association list for. + /// The filtered association list. + protected List BuildActiveAssociationList(PSObject so) + { + var propertyList = parameters?.mshParameterList; + var excludeFilter = parameters?.excludePropertyFilter; + var rawList = BuildRawAssociationList(so, propertyList); + return ApplyExcludeFilter(rawList, excludeFilter); + } + + /// + /// Applies the ExcludeProperty filter to the given association list. + /// + /// The list to filter. + /// The exclude filter to apply. + /// The filtered list, or the original list if no filter is specified. + internal static List ApplyExcludeFilter( + List associationList, + PSPropertyExpressionFilter excludeFilter) + { + if (associationList is null || excludeFilter is null) { - this.activeAssociationList = this.activeAssociationList - .Where(item => !this.parameters.excludePropertyFilter.IsMatch(item.ResolvedExpression)) - .ToList(); + return associationList; } + + return associationList + .Where(item => !excludeFilter.IsMatch(item.ResolvedExpression)) + .ToList(); } protected string GetExpressionDisplayValue(PSObject so, int enumerationLimit, PSPropertyExpression ex, diff --git a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Complex.cs b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Complex.cs index 972a7160830..b81c0c0f860 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Complex.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Complex.cs @@ -3,7 +3,6 @@ using System.Collections; using System.Collections.Generic; -using System.Linq; using System.Collections.ObjectModel; using System.Management.Automation; using System.Management.Automation.Internal; @@ -514,13 +513,8 @@ private void DisplayObject(PSObject so, TraversalInfo currentLevel, List activeAssociationList = AssociationManager.SetupActiveProperties(parameterList, so, _expressionFactory); - // Apply ExcludeProperty filter if specified - if (_parameters != null && _parameters.excludePropertyFilter != null) - { - activeAssociationList = activeAssociationList - .Where(item => !_parameters.excludePropertyFilter.IsMatch(item.ResolvedExpression)) - .ToList(); - } + // Apply ExcludeProperty filter using the centralized method + activeAssociationList = ViewGenerator.ApplyExcludeFilter(activeAssociationList, _parameters?.excludePropertyFilter); // create a format entry FormatEntry fe = new FormatEntry(); diff --git a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_List.cs b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_List.cs index 6ef1b24ce14..35287d7c2e4 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_List.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_List.cs @@ -30,9 +30,14 @@ internal override void Initialize(TerminatingErrorContext errorContext, PSProper { _listBody = (ListControlBody)this.dataBaseInfo.view.mainControl; } + } - this.parameters = parameters; - SetUpActiveProperties(so); + /// + /// Builds the raw association list for list formatting. + /// + protected override List BuildRawAssociationList(PSObject so, List propertyList) + { + return AssociationManager.SetupActiveProperties(propertyList, so, this.expressionFactory); } /// @@ -178,17 +183,14 @@ private ListControlEntryDefinition GetActiveListControlEntryDefinition(ListContr private ListViewEntry GenerateListViewEntryFromProperties(PSObject so, int enumerationLimit) { - // compute active properties every time - if (this.activeAssociationList == null) - { - SetUpActiveProperties(so); - } + // Build active association list (with ExcludeProperty filter applied) + var associationList = BuildActiveAssociationList(so); ListViewEntry lve = new ListViewEntry(); - for (int k = 0; k < this.activeAssociationList.Count; k++) + for (int k = 0; k < associationList.Count; k++) { - MshResolvedExpressionParameterAssociation a = this.activeAssociationList[k]; + MshResolvedExpressionParameterAssociation a = associationList[k]; ListViewField lvf = new ListViewField(); if (a.OriginatingParameter != null) @@ -218,21 +220,7 @@ private ListViewEntry GenerateListViewEntryFromProperties(PSObject so, int enume lvf.formatPropertyField.propertyValue = this.GetExpressionDisplayValue(so, enumerationLimit, a.ResolvedExpression, directive); lve.listViewFieldList.Add(lvf); } - - this.activeAssociationList = null; return lve; } - - private void SetUpActiveProperties(PSObject so) - { - List mshParameterList = null; - - if (this.parameters != null) - mshParameterList = this.parameters.mshParameterList; - - this.activeAssociationList = AssociationManager.SetupActiveProperties(mshParameterList, so, this.expressionFactory); - - ApplyExcludePropertyFilter(); - } } } diff --git a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Table.cs b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Table.cs index 33d2bd53506..3b14c0754ba 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Table.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Table.cs @@ -14,6 +14,8 @@ internal sealed class TableViewGenerator : ViewGenerator // tableBody to use for this instance of the ViewGenerator; private TableControlBody _tableBody; + private List _activeAssociationList; + internal override void Initialize(TerminatingErrorContext terminatingErrorContext, PSPropertyExpressionFactory mshExpressionFactory, TypeInfoDataBase db, ViewDefinition view, FormattingCommandLineParameters formatParameters) { base.Initialize(terminatingErrorContext, mshExpressionFactory, db, view, formatParameters); @@ -34,51 +36,48 @@ internal override void Initialize(TerminatingErrorContext errorContext, PSProper _tableBody = (TableControlBody)this.dataBaseInfo.view.mainControl; } - List rawMshParameterList = null; - - if (parameters != null) - rawMshParameterList = parameters.mshParameterList; + // Build the active association list (with ExcludeProperty filter applied) + _activeAssociationList = BuildActiveAssociationList(so); + } + /// + /// Builds the raw association list for table formatting. + /// + protected override List BuildRawAssociationList(PSObject so, List propertyList) + { // check if we received properties from the command line - if (rawMshParameterList is not null && rawMshParameterList.Count > 0) + if (propertyList is not null && propertyList.Count > 0) { - this.activeAssociationList = AssociationManager.ExpandTableParameters(rawMshParameterList, so); + return AssociationManager.ExpandTableParameters(propertyList, so); } - else + + // we did not get any properties: + // try to get properties from the default property set of the object + var list = AssociationManager.ExpandDefaultPropertySet(so, this.expressionFactory); + if (list.Count > 0) { - // we did not get any properties: - // try to get properties from the default property set of the object - this.activeAssociationList = AssociationManager.ExpandDefaultPropertySet(so, this.expressionFactory); - if (this.activeAssociationList.Count > 0) - { - // we got a valid set of properties from the default property set..add computername for - // remoteobjects (if available) - if (PSObjectHelper.ShouldShowComputerNameProperty(so)) - { - activeAssociationList.Add(new MshResolvedExpressionParameterAssociation(null, - new PSPropertyExpression(RemotingConstants.ComputerNameNoteProperty))); - } - } - else + // we got a valid set of properties from the default property set..add computername for + // remoteobjects (if available) + if (PSObjectHelper.ShouldShowComputerNameProperty(so)) { - // we failed to get anything from the default property set - this.activeAssociationList = AssociationManager.ExpandAll(so); - if (this.activeAssociationList.Count > 0) - { - // Remove PSComputerName and PSShowComputerName from the display as needed. - AssociationManager.HandleComputerNameProperties(so, activeAssociationList); - FilterActiveAssociationList(); - } - else - { - // we were unable to retrieve any properties, so we leave an empty list - this.activeAssociationList = new List(); - return; - } + list.Add(new MshResolvedExpressionParameterAssociation(null, + new PSPropertyExpression(RemotingConstants.ComputerNameNoteProperty))); } + + return list; } - ApplyExcludePropertyFilter(); + // we failed to get anything from the default property set + list = AssociationManager.ExpandAll(so); + if (list.Count > 0) + { + // Remove PSComputerName and PSShowComputerName from the display as needed. + AssociationManager.HandleComputerNameProperties(so, list); + return LimitAssociationListSize(list); + } + + // we were unable to retrieve any properties, so we leave an empty list + return new List(); } /// @@ -129,30 +128,29 @@ internal override FormatStartData GenerateStartData(PSObject so) } /// - /// Method to filter resolved expressions as per table view needs. + /// Limits the association list size for table view. /// For v1.0, table view supports only 10 properties. - /// - /// This method filters and updates "activeAssociationList" instance property. /// - /// None. - /// This method updates "activeAssociationList" instance property. - private void FilterActiveAssociationList() + /// The list to limit. + /// The limited list. + private static List LimitAssociationListSize( + List list) { - // we got a valid set of properties from the default property set - // make sure we do not have too many properties - // NOTE: this is an arbitrary number, chosen to be a sensitive default - const int nMax = 10; + const int maxCount = 10; + + if (list.Count <= maxCount) + { + return list; + } - if (activeAssociationList.Count > nMax) + var result = new List(maxCount); + for (int k = 0; k < maxCount; k++) { - List tmp = this.activeAssociationList; - this.activeAssociationList = new List(); - for (int k = 0; k < nMax; k++) - this.activeAssociationList.Add(tmp[k]); + result.Add(list[k]); } - return; + return result; } private TableHeaderInfo GenerateTableHeaderInfoFromDataBaseInfo(PSObject so) @@ -228,9 +226,9 @@ private TableHeaderInfo GenerateTableHeaderInfoFromProperties(PSObject so) thi.hideHeader = this.HideHeaders; thi.repeatHeader = this.RepeatHeader; - for (int k = 0; k < this.activeAssociationList.Count; k++) + for (int k = 0; k < _activeAssociationList.Count; k++) { - MshResolvedExpressionParameterAssociation a = this.activeAssociationList[k]; + MshResolvedExpressionParameterAssociation a = _activeAssociationList[k]; TableColumnInfo ci = new TableColumnInfo(); // set the label of the column @@ -241,7 +239,7 @@ private TableHeaderInfo GenerateTableHeaderInfoFromProperties(PSObject so) ci.propertyName = (string)key; } - ci.propertyName ??= this.activeAssociationList[k].ResolvedExpression.ToString(); + ci.propertyName ??= _activeAssociationList[k].ResolvedExpression.ToString(); // set the width of the table if (a.OriginatingParameter != null) @@ -473,13 +471,13 @@ private TableRowEntry GenerateTableRowEntryFromDataBaseInfo(PSObject so, int enu private TableRowEntry GenerateTableRowEntryFromFromProperties(PSObject so, int enumerationLimit) { TableRowEntry tre = new TableRowEntry(); - for (int k = 0; k < this.activeAssociationList.Count; k++) + for (int k = 0; k < _activeAssociationList.Count; k++) { FormatPropertyField fpf = new FormatPropertyField(); FieldFormattingDirective directive = null; - if (activeAssociationList[k].OriginatingParameter != null) + if (_activeAssociationList[k].OriginatingParameter != null) { - directive = activeAssociationList[k].OriginatingParameter.GetEntry(FormatParameterDefinitionKeys.FormatStringEntryKey) as FieldFormattingDirective; + directive = _activeAssociationList[k].OriginatingParameter.GetEntry(FormatParameterDefinitionKeys.FormatStringEntryKey) as FieldFormattingDirective; } if (directive is null) @@ -488,7 +486,7 @@ private TableRowEntry GenerateTableRowEntryFromFromProperties(PSObject so, int e directive.isTable = true; } - fpf.propertyValue = this.GetExpressionDisplayValue(so, enumerationLimit, this.activeAssociationList[k].ResolvedExpression, directive); + fpf.propertyValue = this.GetExpressionDisplayValue(so, enumerationLimit, _activeAssociationList[k].ResolvedExpression, directive); tre.formatPropertyFieldList.Add(fpf); } diff --git a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Wide.cs b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Wide.cs index a45c006e7fc..bfa364cc450 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Wide.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Wide.cs @@ -15,6 +15,40 @@ internal override void Initialize(TerminatingErrorContext errorContext, PSProper base.Initialize(errorContext, expressionFactory, so, db, parameters); } + /// + /// Builds the raw association list for wide formatting. + /// + protected override List BuildRawAssociationList(PSObject so, List propertyList) + { + // check if we received properties from the command line + if (propertyList is not null && propertyList.Count > 0) + { + return AssociationManager.ExpandParameters(propertyList, so); + } + + // we did not get any properties: + // try to get the display property of the object + PSPropertyExpression displayNameExpression = PSObjectHelper.GetDisplayNameExpression(so, this.expressionFactory); + if (displayNameExpression is not null) + { + return new List + { + new MshResolvedExpressionParameterAssociation(null, displayNameExpression) + }; + } + + // try to get the default property set (we will use the first property) + var list = AssociationManager.ExpandDefaultPropertySet(so, this.expressionFactory); + if (list.Count == 0) + { + // we failed to get anything from the default property set + // just get all the properties + list = AssociationManager.ExpandAll(so); + } + + return list; + } + internal override FormatStartData GenerateStartData(PSObject so) { FormatStartData startFormat = base.GenerateStartData(so); @@ -129,20 +163,17 @@ private WideControlEntryDefinition GetActiveWideControlEntryDefinition(WideContr private WideViewEntry GenerateWideViewEntryFromProperties(PSObject so, int enumerationLimit) { - // compute active properties every time - if (this.activeAssociationList == null) - { - SetUpActiveProperty(so); - } + // Build active association list (with ExcludeProperty filter applied) + var associationList = BuildActiveAssociationList(so); WideViewEntry wve = new WideViewEntry(); FormatPropertyField fpf = new FormatPropertyField(); wve.formatPropertyField = fpf; - if (this.activeAssociationList.Count > 0) + if (associationList.Count > 0) { // get the first one - MshResolvedExpressionParameterAssociation a = this.activeAssociationList[0]; + MshResolvedExpressionParameterAssociation a = associationList[0]; FieldFormattingDirective directive = null; if (a.OriginatingParameter != null) { @@ -151,44 +182,7 @@ private WideViewEntry GenerateWideViewEntryFromProperties(PSObject so, int enume fpf.propertyValue = this.GetExpressionDisplayValue(so, enumerationLimit, a.ResolvedExpression, directive); } - - this.activeAssociationList = null; return wve; } - - private void SetUpActiveProperty(PSObject so) - { - List rawMshParameterList = this.parameters?.mshParameterList; - - // check if we received properties from the command line - if (rawMshParameterList is not null && rawMshParameterList.Count > 0) - { - this.activeAssociationList = AssociationManager.ExpandParameters(rawMshParameterList, so); - } - else - { - // we did not get any properties: - // try to get the display property of the object - PSPropertyExpression displayNameExpression = PSObjectHelper.GetDisplayNameExpression(so, this.expressionFactory); - if (displayNameExpression is not null) - { - this.activeAssociationList = new List(); - this.activeAssociationList.Add(new MshResolvedExpressionParameterAssociation(null, displayNameExpression)); - } - else - { - // try to get the default property set (we will use the first property) - this.activeAssociationList = AssociationManager.ExpandDefaultPropertySet(so, this.expressionFactory); - if (this.activeAssociationList.Count == 0) - { - // we failed to get anything from the default property set - // just get all the properties - this.activeAssociationList = AssociationManager.ExpandAll(so); - } - } - } - - ApplyExcludePropertyFilter(); - } } } From 7f92d588fa02d58e31ba7c86840f3f1c94bdc466 Mon Sep 17 00:00:00 2001 From: Yoshifumi Date: Thu, 11 Dec 2025 03:21:32 +0900 Subject: [PATCH 2/2] Add ToRegex method to WildcardPattern class (#26515) --- .../engine/regex.cs | 41 +++++++--- .../engine/WildcardPattern.Tests.ps1 | 77 +++++++++++++++++++ 2 files changed, 106 insertions(+), 12 deletions(-) create mode 100644 test/powershell/engine/WildcardPattern.Tests.ps1 diff --git a/src/System.Management.Automation/engine/regex.cs b/src/System.Management.Automation/engine/regex.cs index d69f3bdb88f..6e0ef9741d6 100644 --- a/src/System.Management.Automation/engine/regex.cs +++ b/src/System.Management.Automation/engine/regex.cs @@ -73,18 +73,6 @@ public sealed class WildcardPattern // Default is WildcardOptions.None. internal WildcardOptions Options { get; } - /// - /// Wildcard pattern converted to regex pattern. - /// - internal string PatternConvertedToRegex - { - get - { - var patternRegex = WildcardPatternToRegexParser.Parse(this); - return patternRegex.ToString(); - } - } - /// /// Initializes and instance of the WildcardPattern class /// for the specified wildcard pattern. @@ -205,6 +193,35 @@ public bool IsMatch(string input) return input != null && _isMatch(input); } + /// + /// Converts the wildcard pattern to its regular expression equivalent. + /// + /// + /// A object that represents the regular expression equivalent of the wildcard pattern. + /// The regex is configured with options matching the wildcard pattern's options. + /// + /// + /// This method converts a wildcard pattern to a regular expression. + /// The conversion follows these rules: + /// + /// * (asterisk) converts to .* (matches any string) + /// ? (question mark) converts to . (matches any single character) + /// [abc] (bracket expression) converts to [abc] (matches any character in the set) + /// Literal characters are escaped as needed for regex + /// + /// + /// + /// + /// var pattern = new WildcardPattern("*.txt"); + /// Regex regex = pattern.ToRegex(); + /// // regex.ToString() returns: "\.txt$" + /// + /// + public Regex ToRegex() + { + return WildcardPatternToRegexParser.Parse(this); + } + /// /// Escape special chars, except for those specified in , in a string by replacing them with their escape codes. /// diff --git a/test/powershell/engine/WildcardPattern.Tests.ps1 b/test/powershell/engine/WildcardPattern.Tests.ps1 new file mode 100644 index 00000000000..81ced10f253 --- /dev/null +++ b/test/powershell/engine/WildcardPattern.Tests.ps1 @@ -0,0 +1,77 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe "WildcardPattern.ToRegex Tests" -Tags "CI" { + It "Converts '' to regex pattern ''" -TestCases @( + @{ Pattern = '*.txt'; Expected = '\.txt$' } + @{ Pattern = 'test?.log'; Expected = '^test.\.log$' } + @{ Pattern = 'file[0-9].txt'; Expected = '^file[0-9]\.txt$' } + @{ Pattern = 'test.log'; Expected = '^test\.log$' } + @{ Pattern = '*test*file*.txt'; Expected = 'test.*file.*\.txt$' } + @{ Pattern = 'file[0-9][a-z].txt'; Expected = '^file[0-9][a-z]\.txt$' } + @{ Pattern = 'test*'; Expected = '^test' } + @{ Pattern = '*test*'; Expected = 'test' } + ) { + param($Pattern, $Expected) + $wildcardPattern = [System.Management.Automation.WildcardPattern]::new($Pattern) + $regex = $wildcardPattern.ToRegex() + $regex | Should -BeOfType ([regex]) + $regex.ToString() | Should -BeExactly $Expected + } + + It "Converts '' with option" -TestCases @( + @{ Pattern = 'TEST'; OptionName = 'IgnoreCase'; Option = [System.Management.Automation.WildcardOptions]::IgnoreCase; Expected = '^TEST$'; ExpectedRegexOptions = 'IgnoreCase, Singleline'; TestString = 'test'; ExpectedMatch = $true } + @{ Pattern = 'test'; OptionName = 'CultureInvariant'; Option = [System.Management.Automation.WildcardOptions]::CultureInvariant; Expected = '^test$'; ExpectedRegexOptions = 'Singleline, CultureInvariant'; TestString = 'test'; ExpectedMatch = $true } + @{ Pattern = 'test*'; OptionName = 'Compiled'; Option = [System.Management.Automation.WildcardOptions]::Compiled; Expected = '^test'; ExpectedRegexOptions = 'Compiled, Singleline'; TestString = 'testing'; ExpectedMatch = $true } + ) { + param($Pattern, $OptionName, $Option, $Expected, $ExpectedRegexOptions, $TestString, $ExpectedMatch) + $wildcardPattern = [System.Management.Automation.WildcardPattern]::new($Pattern, $Option) + $regex = $wildcardPattern.ToRegex() + $regex | Should -BeOfType ([regex]) + $regex.ToString() | Should -BeExactly $Expected + $regex.Options.ToString() | Should -BeExactly $ExpectedRegexOptions + $regex.IsMatch($TestString) | Should -Be $ExpectedMatch + } + + It "Regex from '' matches '': " -TestCases @( + @{ Pattern = '*test*file*.txt'; TestString = 'mytestmyfile123.txt'; ShouldMatch = $true } + @{ Pattern = 'file[0-9][a-z].txt'; TestString = 'file5a.txt'; ShouldMatch = $true } + @{ Pattern = 'file[0-9][a-z].txt'; TestString = 'file55.txt'; ShouldMatch = $false } + ) { + param($Pattern, $TestString, $ShouldMatch) + $regex = [System.Management.Automation.WildcardPattern]::new($Pattern).ToRegex() + $regex.IsMatch($TestString) | Should -Be $ShouldMatch + } + + Context "Edge cases" { + It "Handles empty pattern" { + $pattern = [System.Management.Automation.WildcardPattern]::new("") + $regex = $pattern.ToRegex() + $regex | Should -BeOfType ([regex]) + $regex.ToString() | Should -Be "^$" + } + + It "Handles pattern with only asterisk" { + $pattern = [System.Management.Automation.WildcardPattern]::new("*") + $regex = $pattern.ToRegex() + $regex | Should -BeOfType ([regex]) + $regex.ToString() | Should -BeExactly "" + $regex.IsMatch("anything") | Should -BeTrue + $regex.IsMatch("") | Should -BeTrue + } + + It "Handles escaped '' wildcard character" -TestCases @( + @{ Char = '*'; Pattern = 'file`*.txt'; Expected = '^file\*\.txt$' } + @{ Char = '?'; Pattern = 'file`?.txt'; Expected = '^file\?\.txt$' } + @{ Char = '['; Pattern = 'file`[.txt'; Expected = '^file\[\.txt$' } + @{ Char = ']'; Pattern = 'file`].txt'; Expected = '^file]\.txt$' } + ) { + param($Char, $Pattern, $Expected) + $wildcardPattern = [System.Management.Automation.WildcardPattern]::new($Pattern) + $regex = $wildcardPattern.ToRegex() + $regex | Should -BeOfType ([regex]) + $regex.ToString() | Should -BeExactly $Expected + } + + } +}