Tab-completion in custom provider adds spurious repeated path on non-container items #24744
Open
Description
Prerequisites
- Write a descriptive title.
- Make sure you are able to repro it on the latest released version
- Search the existing issues.
- Refer to the FAQ.
- Refer to Differences between Windows PowerShell 5.1 and PowerShell.
Steps to reproduce
Here's some repro code to create a provider where:
- the items are all integers from 0 to 9
- even numbers are containers
- odd numbers are non-container items
multi-file code content that defines IntProvider
-
paket.references:
System.Management.Automation
-
IntProvider.csproj:
<?xml version="1.0" encoding="utf-8"?> <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <Nullable>enable</Nullable> </PropertyGroup> <Import Project=".paket\Paket.Restore.targets" /> </Project>
-
IntProvider.cs:
using System; using System.Linq; using System.Management.Automation; using System.Management.Automation.Provider; namespace BugRepro { public class IntItemInfo { public string Name; public IntItemInfo(string name) => Name = name; } [CmdletProvider("Int", ProviderCapabilities.None)] public class IntProvider : NavigationCmdletProvider { public static string[] ToChunks(string path) => path.Split("/", StringSplitOptions.RemoveEmptyEntries); protected string _ChildName(string path) { var name = ToChunks(path).LastOrDefault(); return name ?? string.Empty; } protected string Normalize(string path) => string.Join("/", ToChunks(path)); protected override string GetChildName(string path) { var name = _ChildName(path); // if (!IsItemContainer(path)) { return string.Empty; } return name; } protected override bool IsValidPath(string path) => int.TryParse(GetChildName(path), out int _); protected override bool IsItemContainer(string path) { var name = _ChildName(path); if (!int.TryParse(name, out int value)) { return false; } if (ToChunks(path).Count() > 3) { return false; } return value % 2 == 0; } protected override bool ItemExists(string path) { foreach (var chunk in ToChunks(path)) { if (!int.TryParse(chunk, out int value)) { return false; } if (value < 0 || value > 9) { return false; } } return true; } protected override void GetItem(string path) { var name = GetChildName(path); if (!int.TryParse(name, out int _)) { return; } WriteItemObject(new IntItemInfo(name), path, IsItemContainer(path)); } protected override bool HasChildItems(string path) => IsItemContainer(path); protected override void GetChildItems(string path, bool recurse) { if (!IsItemContainer(path)) { GetItem(path); return; } for (var i = 0; i <= 9; i++) { var _path = $"{Normalize(path)}/{i}"; if (recurse) { GetChildItems(_path, recurse); } else { GetItem(_path); } } } } }
-
IntProvider.psd1:
@{ RootModule = 'IntProvider.psm1' NestedModules = @('./IntProvider.dll') ModuleVersion = '0.0.1' FunctionsToExport = @() CmdletsToExport = @() VariablesToExport = '*' AliasesToExport = @() }
-
IntProvider.psm1:
if (-not (Test-Path $PSScriptRoot/IntProvider.dll)) { $CS = Get-Content -Raw $PSScriptRoot/IntProvider.cs Add-Type -TypeDefinition $CS -OutputAssembly $PSScriptRoot/IntProvider.dll }
-
To build:
dotnet tool install paket --create-manifest-if-needed dotnet tool restore dotnet build
-
Set up PSDrive:
ipmo IntProvider.psm1 # compile the C# ipmo IntProvider.psd1 # import the assembly - apparently it needs to be a nested module...? New-PSDrive -Name Int -PSProvider Int -Root "/"
Expected behavior
When tab-completing in FileSystemProvider
, no completions are offered as children of non-container items. E.g.:
❯ gi ./IntProvider.csproj/<tab>
Tab-completion strips the trailing slash.
# either the child name, or an exception if the child name is suffixed with dir sep
❯ $inputScript = "gi ./IntProvider.csproj"
❯ [System.Management.Automation.CommandCompletion]::CompleteInput($inputScript, $inputScript.Length, $null).CompletionMatches
CompletionText ListItemText ResultType ToolTip
-------------- ------------ ---------- -------
./IntProvider.csproj IntProvider.csproj ProviderItem /home/freddie/gitroot/BugRepro/IntProvider.csproj
❯ $inputScript = "gi ./IntProvider.csproj/"
❯ [System.Management.Automation.CommandCompletion]::CompleteInput($inputScript, $inputScript.Length, $null).CompletionMatches
MethodInvocationException: Exception calling "CompleteInput" with "3" argument(s): "Could not find a part of the path '/home/freddie/gitroot/BugRepro/IntProvider.csproj'."
Actual behavior
When tab-completing in a custom provider, completions are offered as children of non-container items. E.g.:
❯ gi Int:/2/3/<tab>
completes, incorrectly, to Int:/2/3/2/3
(or to Int:/2/3/3
as pointed out by MartinGC94). Both of these are incorrect.
Tab-completion strips the trailing slash.
# this is correct:
❯ $inputScript = "gi Int:/2/3"
❯ [System.Management.Automation.CommandCompletion]::CompleteInput($inputScript, $inputScript.Length, $null).CompletionMatches
CompletionText ListItemText ResultType ToolTip
-------------- ------------ ---------- -------
Int:/2/3 3 ProviderItem /2/3
# this is the wrong behaviour.
# "3" is not a container, so nothing should be appended
❯ $inputScript = "gi Int:/2/3/"
❯ [System.Management.Automation.CommandCompletion]::CompleteInput($inputScript, $inputScript.Length, $null).CompletionMatches
CompletionText ListItemText ResultType ToolTip
-------------- ------------ ---------- -------
Int:/2/3//2/3/ /2/3/ ProviderItem /2/3//2/3/
Error details
No response
Environment data
Name Value
---- -----
PSVersion 7.4.6
PSEdition Core
GitCommitId 7.4.6
OS Fedora Linux 40 (Workstation Edition)
Platform Unix
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0
# problem also reproduces on master branch, as of c29e9140bf6d47494a3c85b9031db81583b78b20 (2025-01-06)
Visuals
No response