From ef64b23b03f247d9c7b9546263acd3b6d0044cb1 Mon Sep 17 00:00:00 2001 From: kwkam Date: Sat, 13 Jan 2018 15:53:59 +0800 Subject: [PATCH 1/2] [Feature] Fix path handling with wildcard char *System.Management.Automation/engine/CommandCompletion/CompletionCompleters: Use WildcardPattern::Escape to escape completion text of filename. *System.Management.Automation/engine/SessionStateContainer: @Get-ChildItem Unescape non-literal, non-glob path before existence checking. This solve the issue where Get-GhildItem does not behave correctly when -Path contains special characters. For example, > Get-ChildItem -Path './`[dir`]' will complain with error "Cannot find path ..." while > Get-ChildItem -Path './`[dir`]' -Depth 0 will work fine. @New-Item Unescape non-literal path when -Name is not set. This works around for New-Item treating -Path as literal path while it can also be globbable. For example, assuming there is "[file]1" in current directory, and tab completion suggests './`[file`]1' for the -Path argument, but > New-Item -Path './`[file`]2' will create a file named "`[file`]2" instead of "[file]2". *System.Management.Automation/engine/regex: WildcardPattern::Escape should also escape "`" since WildcardPattern::Unescape would unescape it, and the matcher parse it as escape character. *System.Management.Automation/namespaces/LocationGlobber: Do not escape CWD when LiteralPath is used. This causes issues when both -LiteralPath and CWD contains special characters. Do not pass regex escaped string to WildcardPattern::Get. This fails tab completion when doing "./path/to/`[f". *System.Management.Automation/utils/PathUtils: Unescape non-literal, non-glob path before calling GetUnresolvedProviderPathFromPSPath since it only accepts literal path. This solve the issue where some commands depending on that method will treat -Path as literal unintentionally. For example, > Out-File -Path './`[out`].txt' -Append will create a new file named "`[out`].txt" instead of writing to "[out].txt". --- .../CommandCompletion/CompletionCompleters.cs | 20 ++++++------------- .../engine/SessionStateContainer.cs | 6 +++--- .../engine/regex.cs | 5 +++-- .../namespaces/LocationGlobber.cs | 16 ++++++++------- .../utils/PathUtils.cs | 3 ++- 5 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 47e2d74f9b9..34d991bd8d0 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4303,6 +4303,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("'", "''"); @@ -4315,20 +4321,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/SessionStateContainer.cs b/src/System.Management.Automation/engine/SessionStateContainer.cs index 7c29511498f..99e40c08d30 100644 --- a/src/System.Management.Automation/engine/SessionStateContainer.cs +++ b/src/System.Management.Automation/engine/SessionStateContainer.cs @@ -1642,7 +1642,7 @@ internal void GetChildItems( string originalPath = path; path = Globber.GetProviderPath( - path, + context.SuppressWildcardExpansion ? path : WildcardPattern.Unescape(path), context, out provider, out drive); @@ -2689,7 +2689,7 @@ internal void GetChildNames( string providerPath = Globber.GetProviderPath( - path, + context.SuppressWildcardExpansion ? path : WildcardPattern.Unescape(path), context, out provider, out drive); @@ -3798,7 +3798,7 @@ internal void NewItem( if (String.IsNullOrEmpty(name)) { string providerPath = - Globber.GetProviderPath(resolvePath, context, out provider, out driveInfo); + Globber.GetProviderPath(WildcardPattern.Unescape(resolvePath), context, out provider, out driveInfo); providerInstance = GetProviderInstance(provider); providerPaths.Add(providerPath); diff --git a/src/System.Management.Automation/engine/regex.cs b/src/System.Management.Automation/engine/regex.cs index a8039ab2415..01b3af8b053 100644 --- a/src/System.Management.Automation/engine/regex.cs +++ b/src/System.Management.Automation/engine/regex.cs @@ -204,9 +204,10 @@ internal static string Escape(string pattern, char[] charsNotToEscape) char ch = pattern[i]; // - // if it is a wildcard char, escape it + // if it is a wildcard char or escape char, escape it // - if (IsWildcardChar(ch) && !charsNotToEscape.Contains(ch)) + if ((IsWildcardChar(ch) || ch == escapeChar) && + !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 ab586d49997..60f6ebac4dd 100644 --- a/src/System.Management.Automation/namespaces/LocationGlobber.cs +++ b/src/System.Management.Automation/namespaces/LocationGlobber.cs @@ -460,7 +460,13 @@ private Collection ResolveDriveQualifiedPath( s_pathResolutionTracer.WriteLine("Path is DRIVE-QUALIFIED"); - string relativePath = GetDriveRootRelativePathFromPSPath(path, context, true, out drive, out providerInstance); + string relativePath = + GetDriveRootRelativePathFromPSPath( + path, + context, + !context.SuppressWildcardExpansion, + out drive, + out providerInstance); Dbg.Diagnostics.Assert( drive != null, @@ -3630,13 +3636,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 @@ -4270,13 +4274,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/src/System.Management.Automation/utils/PathUtils.cs b/src/System.Management.Automation/utils/PathUtils.cs index 2177f89dbb3..65b89a6f25e 100644 --- a/src/System.Management.Automation/utils/PathUtils.cs +++ b/src/System.Management.Automation/utils/PathUtils.cs @@ -293,7 +293,8 @@ internal static string ResolveFilePath(string filePath, PSCmdlet command, bool i PSDriveInfo drive = null; path = command.SessionState.Path.GetUnresolvedProviderPathFromPSPath( - filePath, cmdletProviderContext, out provider, out drive); + isLiteralPath ? filePath : WildcardPattern.Unescape(filePath), + cmdletProviderContext, out provider, out drive); cmdletProviderContext.ThrowFirstErrorOrDoNothing(); if (!provider.NameEquals(command.Context.ProviderNames.FileSystem)) { From fb57902910b009f1329ff86c5cd1162e576f8089 Mon Sep 17 00:00:00 2001 From: kwkam Date: Mon, 12 Feb 2018 23:07:18 +0800 Subject: [PATCH 2/2] [Feature] Add tests --- .../TabCompletion/TabCompletion.Tests.ps1 | 3 ++ .../LanguageAndParser.TestFollowup.Tests.ps1 | 13 ++++++++ .../Get-ChildItem.Tests.ps1 | 11 +++++++ .../New-Item.Tests.ps1 | 30 ++++++++++++++++++- .../Out-File.Tests.ps1 | 9 ++++++ 5 files changed, 65 insertions(+), 1 deletion(-) diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index 505b11d87eb..ae79fb62a3e 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -739,6 +739,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 = "cd '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/Language/Parser/LanguageAndParser.TestFollowup.Tests.ps1 b/test/powershell/Language/Parser/LanguageAndParser.TestFollowup.Tests.ps1 index da50b2e5541..51d526b3381 100644 --- a/test/powershell/Language/Parser/LanguageAndParser.TestFollowup.Tests.ps1 +++ b/test/powershell/Language/Parser/LanguageAndParser.TestFollowup.Tests.ps1 @@ -303,3 +303,16 @@ Describe "Hash expression with if statement as value" -Tags "CI" { $hash['h'] | Should -BeExactly 'h' } } + +Describe "WildcardPattern" -Tags "CI" { + It "Unescaping escaped string '' should get the original" -TestCases @( + @{inputStr = '*This'} + @{inputStr = 'Is?'} + @{inputStr = 'Real[ly]'} + @{inputStr = 'Ba`sic'} + @{inputStr = 'Test `[more`]?'} + ) { + param($inputStr) + [WildcardPattern]::Unescape([WildcardPattern]::Escape($inputStr)) | Should -BeExactly $inputStr + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/Get-ChildItem.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/Get-ChildItem.Tests.ps1 index d8c42850b64..4f8616af2be 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Management/Get-ChildItem.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Management/Get-ChildItem.Tests.ps1 @@ -21,6 +21,9 @@ Describe "Get-ChildItem" -Tags "CI" { $null = New-Item -Path $TestDrive -Name $item_F -ItemType "File" -Force | ForEach-Object {$_.Attributes = "hidden"} $null = New-Item -Path (Join-Path -Path $TestDrive -ChildPath $item_E) -Name $item_G -ItemType "File" -Force + $specialDirName = "Test[Dir]" + $specialDir = "Test``[Dir``]" + $searchRoot = Join-Path $TestDrive -ChildPath "TestPS" $file1 = Join-Path $searchRoot -ChildPath "D1" -AdditionalChildPath "File1.txt" $file2 = Join-Path $searchRoot -ChildPath "File1.txt" @@ -142,6 +145,14 @@ Describe "Get-ChildItem" -Tags "CI" { (Get-ChildItem -LiteralPath $TestDrive -Depth 1 -Exclude $item_a).Count | Should Be 5 } + It "Should list files in directory contains special char" { + $null = New-Item -Path $TestDrive -Name $specialDirName -ItemType Directory -Force + $specialPath = Join-Path $TestDrive $specialDir + $null = New-Item -Path $specialPath -Name file1.txt -ItemType File -Force + $null = New-Item -Path $specialPath -Name file2.txt -ItemType File -Force + (Get-ChildItem -Path $specialPath).Count | Should -Be 2 + } + It "get-childitem path wildcard - " -TestCases $PathWildCardTestCases { param($Parameters, $ExpectedCount) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/New-Item.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/New-Item.Tests.ps1 index 13d2421d5bc..49cc78c620c 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Management/New-Item.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Management/New-Item.Tests.ps1 @@ -31,16 +31,17 @@ function Clean-State Describe "New-Item" -Tags "CI" { $tmpDirectory = $TestDrive $testfile = "testfile.txt" + $testfileSp = "``[test``]file.txt" $testfolder = "newDirectory" $testsubfolder = "newSubDirectory" $testlink = "testlink" $FullyQualifiedFile = Join-Path -Path $tmpDirectory -ChildPath $testfile + $FullyQualifiedFileSp = Join-Path -Path $tmpDirectory -ChildPath $testfileSp $FullyQualifiedFolder = Join-Path -Path $tmpDirectory -ChildPath $testfolder $FullyQualifiedLink = Join-Path -Path $tmpDirectory -ChildPath $testlink $FullyQualifiedSubFolder = Join-Path -Path $FullyQualifiedFolder -ChildPath $testsubfolder $FullyQualifiedFileInFolder = Join-Path -Path $FullyQualifiedFolder -ChildPath $testfile - BeforeEach { Clean-State } @@ -100,6 +101,12 @@ Describe "New-Item" -Tags "CI" { Test-Path $FullyQualifiedFile | Should -BeTrue } + It "Should create a file with correct name when Name switch is not used and Path contains special char" { + New-Item -Path $FullyQualifiedFileSp -ItemType file + + $FullyQualifiedFileSp | Should -Exist + } + It "Should be able to create a multiple items in different directories" { $FullyQualifiedFile2 = Join-Path -Path $tmpDirectory -ChildPath test2.txt New-Item -ItemType file -Path $FullyQualifiedFile, $FullyQualifiedFile2 @@ -193,9 +200,15 @@ Describe "New-Item with links" -Tags @('CI', 'RequireAdminOnWindows') { $testfile = "testfile.txt" $testfolder = "newDirectory" $testlink = "testlink" + $testlinkSrcSpName = "[test]src" + $testlinkSrcSp = "``[test``]src" + $testlinkSpName = "[test]link" + $testlinkSp = "``[test``]link" $FullyQualifiedFile = Join-Path -Path $tmpDirectory -ChildPath $testfile $FullyQualifiedFolder = Join-Path -Path $tmpDirectory -ChildPath $testfolder $FullyQualifiedLink = Join-Path -Path $tmpDirectory -ChildPath $testlink + $FullyQualifiedLSrcSp = Join-Path -Path $tmpDirectory -ChildPath $testlinkSrcSp + $FullyQualifiedLinkSp = Join-Path -Path $tmpDirectory -ChildPath $testlinkSp $SymLinkMask = [System.IO.FileAttributes]::ReparsePoint $DirLinkMask = $SymLinkMask -bor [System.IO.FileAttributes]::Directory @@ -246,6 +259,21 @@ Describe "New-Item with links" -Tags @('CI', 'RequireAdminOnWindows') { Test-Path $FullyQualifiedLink | Should -BeFalse } + It "Should create symbolic link with name contains special char" { + $null = New-Item -Path $tmpDirectory -Name $testlinkSrcSpName -ItemType File + $FullyQualifiedLSrcSp | Should -Exist + + $null = New-Item -Path $FullyQualifiedLinkSp -Target $FullyQualifiedLSrcSp -ItemType SymbolicLink + $FullyQualifiedLinkSp | Should -Exist + + $expectedTarget = Join-Path $tmpDirectory $testlinkSrcSpName + + $fileInfo = Get-ChildItem $FullyQualifiedLinkSp + $fileInfo.Target | Should -Match ([regex]::Escape($expectedTarget)) + $fileInfo.LinkType | Should -BeExactly "SymbolicLink" + $fileInfo.Attributes -band $DirLinkMask | Should -BeExactly $SymLinkMask + } + It "New-Item -ItemType SymbolicLink should understand directory path ending with slash" { $folderName = [System.IO.Path]::GetRandomFileName() $symbolicLinkPath = New-Item -ItemType SymbolicLink -Path "$tmpDirectory/$folderName/" -Value "/bar/" diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Out-File.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Out-File.Tests.ps1 index b438c30a372..c58b7874bdb 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Out-File.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Out-File.Tests.ps1 @@ -24,6 +24,7 @@ Describe "Out-File" -Tags "CI" { $expectedContent = "some test text" $inObject = New-Object psobject -Property @{text=$expectedContent} $testfile = Join-Path -Path $TestDrive -ChildPath outfileTest.txt + $testfileSp = Join-Path -Path $TestDrive -ChildPath "``[outfileTest``].txt" } AfterEach { @@ -91,6 +92,14 @@ Describe "Out-File" -Tags "CI" { $actual[11] | Should -BeNullOrEmpty } + It "Should create the file with correct name when FilePath contains special char" { + Out-File -FilePath $testfile + Out-File -FilePath $testfileSp + + $testfile | Should -Exist + $testfileSp | Should -Exist + } + It "Should limit each line to the specified number of characters when the width switch is used on objects" { Out-File -FilePath $testfile -Width 10 -InputObject $inObject