diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 39a9860df95..cfffd1a1d6a 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4061,10 +4061,8 @@ internal static IEnumerable CompleteFilename(CompletionContext var relativePaths = context.GetOption("RelativePaths", @default: defaultRelative); var useLiteralPath = context.GetOption("LiteralPaths", @default: false); - if (useLiteralPath && LocationGlobber.StringContainsGlobCharacters(wordToComplete)) - { - wordToComplete = WildcardPattern.Escape(wordToComplete, Utils.Separators.StarOrQuestion); - } + // We should always escape '[' and ']' if present. + wordToComplete = WildcardPattern.Escape(wordToComplete, Utils.Separators.StarOrQuestion); if (!defaultRelative && wordToComplete.Length >= 2 && wordToComplete[1] == ':' && char.IsLetter(wordToComplete[0]) && executionContext != null) { @@ -4073,6 +4071,43 @@ internal static IEnumerable CompleteFilename(CompletionContext executionContext.SessionState.Drive.GetAtScope(wordToComplete.Substring(0, 1), "global"); } +#if UNIX + // We use globbing to get completions. + // Globbing is based on system functions which is case-sensitive on Unix. + // To make globbing on Unix case-insensitive we transform every char from the input string: + // "alias:dir" -> "alias:[dD][iI][rR]" + if (context.GetOption("UnixCaseInsensitiveGlobbing", @default: true) == true) + { + StringBuilder sb = new StringBuilder(wordToComplete.Length * 4); + + //wordToComplete = WildcardPattern.Escape(wordToComplete, Utils.Separators.Brackets); + + // Copy a provider name "as is" without transformation. + var providerPrefixIndex = wordToComplete.IndexOf(':') + 1; + int i; + for (i=0; i < providerPrefixIndex; i++) + { + sb.Append(wordToComplete[i]); + } + + // Transform rest of input string after a provider prefix. + for (var j=i; j < wordToComplete.Length; j++) + { + var lch = Char.ToLower(wordToComplete[j]); + var uch = Char.ToUpper(wordToComplete[j]); + if (lch != uch) + { + sb.Append('[').Append(lch).Append(uch).Append(']'); + } + else + { + sb.Append(wordToComplete[j]); + } + } + + wordToComplete = sb.ToString(); + } +#endif var powerShellExecutionHelper = context.Helper; powerShellExecutionHelper .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Resolve-Path") @@ -4215,7 +4250,7 @@ internal static IEnumerable CompleteFilename(CompletionContext } // Sorting the results by the path - var sortedPsobjs = psobjs.OrderBy(a => a, new ItemPathComparer()); + var sortedPsobjs = psobjs.OrderBy(a => a, new ItemPathComparer(wordToComplete)); foreach (PSObject psobj in sortedPsobjs) { @@ -6637,6 +6672,13 @@ internal static bool IsAmpersandNeeded(CompletionContext context, bool defaultCh private class ItemPathComparer : IComparer { + private String _baseWord; + + public ItemPathComparer(String baseWord) + { + _baseWord = baseWord + ".*"; + } + public int Compare(PSObject x, PSObject y) { var xPathInfo = PSObject.Base(x) as PathInfo; @@ -6666,7 +6708,21 @@ public int Compare(PSObject x, PSObject y) if (string.IsNullOrEmpty(xPath) || string.IsNullOrEmpty(yPath)) Diagnostics.Assert(false, "Base object of item PSObject should be either PathInfo or FileSystemInfo"); - return String.Compare(xPath, yPath, StringComparison.CurrentCultureIgnoreCase); + var result = String.Compare(xPath, yPath, StringComparison.CurrentCultureIgnoreCase); +#if UNIX + if (result == 0) + { + if (Regex.IsMatch(xPath, _baseWord)) + { + return -1; + } + else + { + return 1; + } + } +#endif + return result; } } diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index b199918dd7b..f9ec57bb656 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -197,8 +197,12 @@ Describe "TabCompletion" -Tags CI { Context "File name completion" { BeforeAll { + $oneSubDirLowerTest = "onesubd" + $oneSubDirUpperTest = "oneSubD" + $oneSubDirTest = "onesubdir" + $tempDir = Join-Path -Path $TestDrive -ChildPath "baseDir" - $oneSubDir = Join-Path -Path $tempDir -ChildPath "oneSubDir" + $oneSubDir = Join-Path -Path $tempDir -ChildPath $oneSubDirTest $oneSubDirPrime = Join-Path -Path $tempDir -ChildPath "prime" $twoSubDir = Join-Path -Path $oneSubDir -ChildPath "twoSubDir" @@ -207,6 +211,13 @@ Describe "TabCompletion" -Tags CI { New-Item -Path $oneSubDirPrime -ItemType Directory -Force > $null New-Item -Path $twoSubDir -ItemType Directory -Force > $null + if (!$IsWindows) { + $oneSubDirTest2 = "oneSubDir" + $oneSubDir2 = Join-Path -Path $tempDir -ChildPath $oneSubDirTest2 + New-Item -Path $oneSubDir2 -ItemType Directory -Force > $null + } + + $testCases = @( @{ inputStr = "ab"; name = "abc"; localExpected = ".${separator}abc"; oneSubExpected = "..${separator}abc"; twoSubExpected = "..${separator}..${separator}abc" } @{ inputStr = "asaasas"; name = "asaasas!popee"; localExpected = ".${separator}asaasas!popee"; oneSubExpected = "..${separator}asaasas!popee"; twoSubExpected = "..${separator}..${separator}asaasas!popee" } @@ -241,6 +252,28 @@ Describe "TabCompletion" -Tags CI { Pop-Location } + It "TabCompletion should be case-insensitive for file names on Windows" -Skip:(!$IsWindows) { + Push-Location -Path $tempDir + $res = TabExpansion2 -inputScript $oneSubDirLowerTest -cursorColumn $oneSubDirLowerTest.Length + $res.CompletionMatches.Count | Should BeGreaterThan 0 + $res.CompletionMatches[0].CompletionText | Should Be ".${separator}$oneSubDirTest" + + $res = TabExpansion2 -inputScript $oneSubDirUpperTest -cursorColumn $oneSubDirUpperTest.Length + $res.CompletionMatches.Count | Should BeGreaterThan 0 + $res.CompletionMatches[0].CompletionText | Should Be ".${separator}$oneSubDirTest" + } + + It "TabCompletion should be case-sensitive for file names on Unix" -Skip:($IsWindows) { + Push-Location -Path $tempDir + $res = TabExpansion2 -inputScript $oneSubDirLowerTest -cursorColumn $oneSubDirLowerTest.Length + $res.CompletionMatches.Count | Should BeGreaterThan 0 + $res.CompletionMatches[0].CompletionText | Should BeExactly ".${separator}$oneSubDirTest" + + $res = TabExpansion2 -inputScript $oneSubDirUpperTest -cursorColumn $oneSubDirUpperTest.Length + $res.CompletionMatches.Count | Should BeGreaterThan 0 + $res.CompletionMatches[0].CompletionText | Should BeExactly ".${separator}$oneSubDirTest2" + } + It "Input '' should successfully complete" -TestCases $testCases { param ($inputStr, $localExpected) @@ -478,8 +511,8 @@ Describe "TabCompletion" -Tags CI { $beforeTab = 'filesystem::{0}\Wind' -f $env:SystemDrive $afterTab = 'filesystem::{0}\Windows' -f $env:SystemDrive } else { - $beforeTab = 'filesystem::/us' -f $env:SystemDrive - $afterTab = 'filesystem::/usr' -f $env:SystemDrive + $beforeTab = 'filesystem::/sbi' -f $env:SystemDrive + $afterTab = 'filesystem::/sbin' -f $env:SystemDrive } $res = TabExpansion2 -inputScript $beforeTab -cursorColumn $beforeTab.Length $res.CompletionMatches.Count | Should BeGreaterThan 0