diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index e00302a1eb4..65974c600c3 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4398,6 +4398,12 @@ internal static IEnumerable CompleteFilename(CompletionContext if (CompletionRequiresQuotes(completionText, !useLiteralPath)) { var quoteInUse = quote == string.Empty ? "'" : quote; + + if (!useLiteralPath) + { + completionText = WildcardPattern.Escape(completionText); + } + if (quoteInUse == "'") { completionText = completionText.Replace("'", "''"); @@ -4410,20 +4416,6 @@ internal static IEnumerable CompleteFilename(CompletionContext completionText = completionText.Replace("$", "`$"); } - if (!useLiteralPath) - { - if (quoteInUse == "'") - { - completionText = completionText.Replace("[", "`["); - completionText = completionText.Replace("]", "`]"); - } - else - { - completionText = completionText.Replace("[", "``["); - completionText = completionText.Replace("]", "``]"); - } - } - completionText = quoteInUse + completionText + quoteInUse; } else if (quote != string.Empty) diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs index f6993a03a0a..cd3b54ef941 100644 --- a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs +++ b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs @@ -109,6 +109,9 @@ static ExperimentalFeature() new ExperimentalFeature( name: "PSCommandNotFoundSuggestion", description: "Recommend potential commands based on fuzzy search on a CommandNotFoundException"), + new ExperimentalFeature( + name: "PSWildcardEscapeEscape", + description: "Fix WildcardPattern API: escape the escape character"), #if UNIX new ExperimentalFeature( name: "PSUnixFileStat", diff --git a/src/System.Management.Automation/engine/regex.cs b/src/System.Management.Automation/engine/regex.cs index c867f355158..26eb76ed649 100644 --- a/src/System.Management.Automation/engine/regex.cs +++ b/src/System.Management.Automation/engine/regex.cs @@ -232,15 +232,28 @@ internal static string Escape(string pattern, char[] charsNotToEscape) Span temp = pattern.Length < StackAllocThreshold ? stackalloc char[pattern.Length * 2 + 1] : new char[pattern.Length * 2 + 1]; int tempIndex = 0; + bool charNeedsEscaping = false; for (int i = 0; i < pattern.Length; i++) { char ch = pattern[i]; - // - // if it is a wildcard char, escape it - // - if (IsWildcardChar(ch) && !charsNotToEscape.Contains(ch)) + if (ExperimentalFeature.IsEnabled("PSWildcardEscapeEscape")) + { + // + // if it is a wildcard char or escape char, escape it + // + charNeedsEscaping = IsWildcardChar(ch) || ch == escapeChar; + } + else + { + // + // if it is a wildcard char, escape it + // + charNeedsEscaping = IsWildcardChar(ch); + } + + if (charNeedsEscaping && !charsNotToEscape.Contains(ch)) { temp[tempIndex++] = escapeChar; } diff --git a/src/System.Management.Automation/namespaces/LocationGlobber.cs b/src/System.Management.Automation/namespaces/LocationGlobber.cs index ab43d492d8f..166608ec8b5 100644 --- a/src/System.Management.Automation/namespaces/LocationGlobber.cs +++ b/src/System.Management.Automation/namespaces/LocationGlobber.cs @@ -3360,13 +3360,11 @@ private List GenerateNewPSPathsWithGlobLeaf( StringContainsGlobCharacters(leafElement) || isLastLeaf) { - string regexEscapedLeafElement = ConvertMshEscapeToRegexEscape(leafElement); - // Construct the glob filter WildcardPattern stringMatcher = WildcardPattern.Get( - regexEscapedLeafElement, + leafElement, WildcardOptions.IgnoreCase); // Construct the include filter @@ -3966,13 +3964,11 @@ internal List GenerateNewPathsWithGlobLeaf( (StringContainsGlobCharacters(leafElement) || isLastLeaf)) { - string regexEscapedLeafElement = ConvertMshEscapeToRegexEscape(leafElement); - // Construct the glob filter WildcardPattern stringMatcher = WildcardPattern.Get( - regexEscapedLeafElement, + leafElement, WildcardOptions.IgnoreCase); // Construct the include filter diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index b8b1cc607a5..fae5e4f0d4a 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -1055,6 +1055,9 @@ dir -Recurse ` @{ inputStr = "Get-Process >'.\My ``[Path``]\'"; expected = "'.${separator}My ``[Path``]${separator}test.ps1'" } @{ inputStr = "Get-Process >${tempDir}\My"; expected = "'${tempDir}${separator}My ``[Path``]'" } @{ inputStr = "Get-Process > '${tempDir}\My ``[Path``]\'"; expected = "'${tempDir}${separator}My ``[Path``]${separator}test.ps1'" } + @{ inputStr = "Set-Location -Path 'My ``["; expected = "'.${separator}My ``[Path``]'" } + @{ inputStr = "Get-Process > 'My ``["; expected = "'.${separator}My ``[Path``]'" } + @{ inputStr = "Get-Process > '${tempDir}\My ``["; expected = "'${tempDir}${separator}My ``[Path``]'" } ) Push-Location -Path $tempDir diff --git a/test/powershell/engine/Api/WildcardPattern.Tests.ps1 b/test/powershell/engine/Api/WildcardPattern.Tests.ps1 new file mode 100644 index 00000000000..ef2688fed53 --- /dev/null +++ b/test/powershell/engine/Api/WildcardPattern.Tests.ps1 @@ -0,0 +1,68 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +Describe "WildcardPattern Escape - Experimental-Feature-Disabled" -Tags "CI" { + + BeforeAll { + $testName = 'PSWildcardEscapeEscape' + $skipTest = $EnabledExperimentalFeatures.Contains($testName) + + if ($skipTest) { + Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature '$testName' to be disabled." -Verbose + $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + $PSDefaultParameterValues["it:skip"] = $true + } + } + + AfterAll { + if ($skipTest) { + $global:PSDefaultParameterValues = $originalDefaultParameterValues + } + } + + It "Unescaping '' which escaped from '' should get the original" -TestCases @( + @{inputStr = '*This'; escapedStr = '`*This'} + @{inputStr = 'Is?'; escapedStr = 'Is`?'} + @{inputStr = 'Real[ly]'; escapedStr = 'Real`[ly`]'} + @{inputStr = 'Ba`sic'; escapedStr = 'Ba`sic'} + @{inputStr = 'Test `[more`]?'; escapedStr = 'Test ``[more``]`?'} + ) { + param($inputStr, $escapedStr) + + [WildcardPattern]::Escape($inputStr) | Should -BeExactly $escapedStr + [WildcardPattern]::Unescape($escapedStr) | Should -BeExactly $inputStr + } +} + +Describe "WildcardPattern Escape - Experimental-Feature-Enabled" -Tags "CI" { + + BeforeAll { + $testName = 'PSWildcardEscapeEscape' + $skipTest = -not $EnabledExperimentalFeatures.Contains($testName) + + if ($skipTest) { + Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature '$testName' to be enabled." -Verbose + $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + $PSDefaultParameterValues["it:skip"] = $true + } + } + + AfterAll { + if ($skipTest) { + $global:PSDefaultParameterValues = $originalDefaultParameterValues + } + } + + It "Unescaping '' which escaped from '' should get the original" -TestCases @( + @{inputStr = '*This'; escapedStr = '`*This'} + @{inputStr = 'Is?'; escapedStr = 'Is`?'} + @{inputStr = 'Real[ly]'; escapedStr = 'Real`[ly`]'} + @{inputStr = 'Ba`sic'; escapedStr = 'Ba``sic'} + @{inputStr = 'Test `[more`]?'; escapedStr = 'Test ```[more```]`?'} + ) { + param($inputStr, $escapedStr) + + [WildcardPattern]::Escape($inputStr) | Should -BeExactly $escapedStr + [WildcardPattern]::Unescape($escapedStr) | Should -BeExactly $inputStr + } +} diff --git a/test/tools/TestMetadata.json b/test/tools/TestMetadata.json index f8439ced074..039100000b4 100644 --- a/test/tools/TestMetadata.json +++ b/test/tools/TestMetadata.json @@ -1,5 +1,6 @@ { "ExperimentalFeatures": { + "PSWildcardEscapeEscape": [ "test/powershell/engine/Api/WildcardPattern.Tests.ps1" ], "ExpTest.FeatureOne": [ "test/powershell/engine/ExperimentalFeature/ExperimentalFeature.Basic.Tests.ps1" ], "PSNullConditionalOperators": [ "test/powershell/Language/Operators/NullConditional.Tests.ps1",