From 0e0e0dce789bfe6b0802f73badfe7925f90bf592 Mon Sep 17 00:00:00 2001 From: Stuart Lang Date: Fri, 17 Apr 2026 00:46:47 +0100 Subject: [PATCH 01/22] Add System.CommandLine.StaticCompletions project Imports the StaticCompletions library and test project from dotnet/sdk, converting the System.CommandLine dependency from a NuGet PackageReference to a ProjectReference. Tests run against a real System.CommandLine build via Verify.Xunit snapshots. --- Directory.Packages.props | 2 + System.CommandLine.sln | 30 ++ .../BashShellProviderTests.cs | 45 +++ .../Directory.Build.targets | 11 + .../GlobalUsings.cs | 6 + .../HelpExtensionsTests.cs | 47 +++ .../PowershellProviderTests.cs | 47 +++ ...CommandLine.StaticCompletions.Tests.csproj | 24 ++ .../VerifyConfiguration.cs | 29 ++ .../VerifyExtensions.cs | 18 + .../ZshShellProviderTests.cs | 89 +++++ ...oviderTests.GenericCompletions.verified.sh | 20 + ...sts.NestedSubcommandCompletion.verified.sh | 68 ++++ ...erTests.SimpleOptionCompletion.verified.sh | 20 + ...commandAndOptionInTopLevelList.verified.sh | 44 ++ ...viderTests.GenericCompletions.verified.ps1 | 31 ++ ...ts.NestedSubcommandCompletion.verified.ps1 | 45 +++ ...rTests.SimpleOptionCompletion.verified.ps1 | 32 ++ ...ommandAndOptionInTopLevelList.verified.ps1 | 39 ++ ...omStaticCompletionsGeneration.verified.zsh | 34 ++ ....DynamicCompletionsGeneration.verified.zsh | 44 ++ ...viderTests.GenericCompletions.verified.zsh | 104 +++++ .../CompletionsCommandDefinition.cs | 38 ++ .../CompletionsCommandParser.cs | 44 ++ ...pletionsGenerateScriptCommandDefinition.cs | 18 + .../DynamicSymbolExtensions.cs | 66 +++ .../HelpGenerationExtensions.cs | 115 ++++++ .../Resources/Strings.resx | 156 ++++++++ .../Resources/xlf/Strings.cs.xlf | 57 +++ .../Resources/xlf/Strings.de.xlf | 57 +++ .../Resources/xlf/Strings.es.xlf | 57 +++ .../Resources/xlf/Strings.fr.xlf | 57 +++ .../Resources/xlf/Strings.it.xlf | 57 +++ .../Resources/xlf/Strings.ja.xlf | 57 +++ .../Resources/xlf/Strings.ko.xlf | 57 +++ .../Resources/xlf/Strings.pl.xlf | 57 +++ .../Resources/xlf/Strings.pt-BR.xlf | 57 +++ .../Resources/xlf/Strings.ru.xlf | 57 +++ .../Resources/xlf/Strings.tr.xlf | 57 +++ .../Resources/xlf/Strings.zh-Hans.xlf | 57 +++ .../Resources/xlf/Strings.zh-Hant.xlf | 57 +++ .../ShellName.cs | 43 ++ ...ystem.CommandLine.StaticCompletions.csproj | 33 ++ .../shells/BashShellProvider.cs | 226 +++++++++++ .../shells/FishShellProvider.cs | 28 ++ .../shells/NuShellShellProvider.cs | 49 +++ .../shells/PowershellShellProvider.cs | 246 ++++++++++++ .../shells/ShellProvider.cs | 35 ++ .../shells/ZshShellProvider.cs | 377 ++++++++++++++++++ 49 files changed, 3044 insertions(+) create mode 100644 src/System.CommandLine.StaticCompletions.Tests/BashShellProviderTests.cs create mode 100644 src/System.CommandLine.StaticCompletions.Tests/Directory.Build.targets create mode 100644 src/System.CommandLine.StaticCompletions.Tests/GlobalUsings.cs create mode 100644 src/System.CommandLine.StaticCompletions.Tests/HelpExtensionsTests.cs create mode 100644 src/System.CommandLine.StaticCompletions.Tests/PowershellProviderTests.cs create mode 100644 src/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj create mode 100644 src/System.CommandLine.StaticCompletions.Tests/VerifyConfiguration.cs create mode 100644 src/System.CommandLine.StaticCompletions.Tests/VerifyExtensions.cs create mode 100644 src/System.CommandLine.StaticCompletions.Tests/ZshShellProviderTests.cs create mode 100644 src/System.CommandLine.StaticCompletions.Tests/snapshots/bash/BashShellProviderTests.GenericCompletions.verified.sh create mode 100644 src/System.CommandLine.StaticCompletions.Tests/snapshots/bash/BashShellProviderTests.NestedSubcommandCompletion.verified.sh create mode 100644 src/System.CommandLine.StaticCompletions.Tests/snapshots/bash/BashShellProviderTests.SimpleOptionCompletion.verified.sh create mode 100644 src/System.CommandLine.StaticCompletions.Tests/snapshots/bash/BashShellProviderTests.SubcommandAndOptionInTopLevelList.verified.sh create mode 100644 src/System.CommandLine.StaticCompletions.Tests/snapshots/pwsh/PowershellProviderTests.GenericCompletions.verified.ps1 create mode 100644 src/System.CommandLine.StaticCompletions.Tests/snapshots/pwsh/PowershellProviderTests.NestedSubcommandCompletion.verified.ps1 create mode 100644 src/System.CommandLine.StaticCompletions.Tests/snapshots/pwsh/PowershellProviderTests.SimpleOptionCompletion.verified.ps1 create mode 100644 src/System.CommandLine.StaticCompletions.Tests/snapshots/pwsh/PowershellProviderTests.SubcommandAndOptionInTopLevelList.verified.ps1 create mode 100644 src/System.CommandLine.StaticCompletions.Tests/snapshots/zsh/ZshShellProviderTests.CustomStaticCompletionsGeneration.verified.zsh create mode 100644 src/System.CommandLine.StaticCompletions.Tests/snapshots/zsh/ZshShellProviderTests.DynamicCompletionsGeneration.verified.zsh create mode 100644 src/System.CommandLine.StaticCompletions.Tests/snapshots/zsh/ZshShellProviderTests.GenericCompletions.verified.zsh create mode 100644 src/System.CommandLine.StaticCompletions/CompletionsCommandDefinition.cs create mode 100644 src/System.CommandLine.StaticCompletions/CompletionsCommandParser.cs create mode 100644 src/System.CommandLine.StaticCompletions/CompletionsGenerateScriptCommandDefinition.cs create mode 100644 src/System.CommandLine.StaticCompletions/DynamicSymbolExtensions.cs create mode 100644 src/System.CommandLine.StaticCompletions/HelpGenerationExtensions.cs create mode 100644 src/System.CommandLine.StaticCompletions/Resources/Strings.resx create mode 100644 src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.cs.xlf create mode 100644 src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.de.xlf create mode 100644 src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.es.xlf create mode 100644 src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.fr.xlf create mode 100644 src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.it.xlf create mode 100644 src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.ja.xlf create mode 100644 src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.ko.xlf create mode 100644 src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.pl.xlf create mode 100644 src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.pt-BR.xlf create mode 100644 src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.ru.xlf create mode 100644 src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.tr.xlf create mode 100644 src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.zh-Hans.xlf create mode 100644 src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.zh-Hant.xlf create mode 100644 src/System.CommandLine.StaticCompletions/ShellName.cs create mode 100644 src/System.CommandLine.StaticCompletions/System.CommandLine.StaticCompletions.csproj create mode 100644 src/System.CommandLine.StaticCompletions/shells/BashShellProvider.cs create mode 100644 src/System.CommandLine.StaticCompletions/shells/FishShellProvider.cs create mode 100644 src/System.CommandLine.StaticCompletions/shells/NuShellShellProvider.cs create mode 100644 src/System.CommandLine.StaticCompletions/shells/PowershellShellProvider.cs create mode 100644 src/System.CommandLine.StaticCompletions/shells/ShellProvider.cs create mode 100644 src/System.CommandLine.StaticCompletions/shells/ZshShellProvider.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index da1e624248..89021a0760 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,6 +19,8 @@ + + diff --git a/System.CommandLine.sln b/System.CommandLine.sln index 88ff8abd06..90e6e95f74 100644 --- a/System.CommandLine.sln +++ b/System.CommandLine.sln @@ -31,6 +31,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-suggest.Tests", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.ApiCompatibility.Tests", "src\System.CommandLine.ApiCompatibility.Tests\System.CommandLine.ApiCompatibility.Tests.csproj", "{A54EE328-D456-4BAF-A180-84E77E6409AC}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.StaticCompletions", "src\System.CommandLine.StaticCompletions\System.CommandLine.StaticCompletions.csproj", "{B1C3A2F4-5D6E-7F8A-9B0C-1D2E3F4A5B6C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.StaticCompletions.Tests", "src\System.CommandLine.StaticCompletions.Tests\System.CommandLine.StaticCompletions.Tests.csproj", "{C2D3E4F5-6A7B-8C9D-0E1F-2A3B4C5D6E7F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -101,6 +105,30 @@ Global {A54EE328-D456-4BAF-A180-84E77E6409AC}.Release|x64.Build.0 = Release|Any CPU {A54EE328-D456-4BAF-A180-84E77E6409AC}.Release|x86.ActiveCfg = Release|Any CPU {A54EE328-D456-4BAF-A180-84E77E6409AC}.Release|x86.Build.0 = Release|Any CPU + {B1C3A2F4-5D6E-7F8A-9B0C-1D2E3F4A5B6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1C3A2F4-5D6E-7F8A-9B0C-1D2E3F4A5B6C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1C3A2F4-5D6E-7F8A-9B0C-1D2E3F4A5B6C}.Debug|x64.ActiveCfg = Debug|Any CPU + {B1C3A2F4-5D6E-7F8A-9B0C-1D2E3F4A5B6C}.Debug|x64.Build.0 = Debug|Any CPU + {B1C3A2F4-5D6E-7F8A-9B0C-1D2E3F4A5B6C}.Debug|x86.ActiveCfg = Debug|Any CPU + {B1C3A2F4-5D6E-7F8A-9B0C-1D2E3F4A5B6C}.Debug|x86.Build.0 = Debug|Any CPU + {B1C3A2F4-5D6E-7F8A-9B0C-1D2E3F4A5B6C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1C3A2F4-5D6E-7F8A-9B0C-1D2E3F4A5B6C}.Release|Any CPU.Build.0 = Release|Any CPU + {B1C3A2F4-5D6E-7F8A-9B0C-1D2E3F4A5B6C}.Release|x64.ActiveCfg = Release|Any CPU + {B1C3A2F4-5D6E-7F8A-9B0C-1D2E3F4A5B6C}.Release|x64.Build.0 = Release|Any CPU + {B1C3A2F4-5D6E-7F8A-9B0C-1D2E3F4A5B6C}.Release|x86.ActiveCfg = Release|Any CPU + {B1C3A2F4-5D6E-7F8A-9B0C-1D2E3F4A5B6C}.Release|x86.Build.0 = Release|Any CPU + {C2D3E4F5-6A7B-8C9D-0E1F-2A3B4C5D6E7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2D3E4F5-6A7B-8C9D-0E1F-2A3B4C5D6E7F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2D3E4F5-6A7B-8C9D-0E1F-2A3B4C5D6E7F}.Debug|x64.ActiveCfg = Debug|Any CPU + {C2D3E4F5-6A7B-8C9D-0E1F-2A3B4C5D6E7F}.Debug|x64.Build.0 = Debug|Any CPU + {C2D3E4F5-6A7B-8C9D-0E1F-2A3B4C5D6E7F}.Debug|x86.ActiveCfg = Debug|Any CPU + {C2D3E4F5-6A7B-8C9D-0E1F-2A3B4C5D6E7F}.Debug|x86.Build.0 = Debug|Any CPU + {C2D3E4F5-6A7B-8C9D-0E1F-2A3B4C5D6E7F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2D3E4F5-6A7B-8C9D-0E1F-2A3B4C5D6E7F}.Release|Any CPU.Build.0 = Release|Any CPU + {C2D3E4F5-6A7B-8C9D-0E1F-2A3B4C5D6E7F}.Release|x64.ActiveCfg = Release|Any CPU + {C2D3E4F5-6A7B-8C9D-0E1F-2A3B4C5D6E7F}.Release|x64.Build.0 = Release|Any CPU + {C2D3E4F5-6A7B-8C9D-0E1F-2A3B4C5D6E7F}.Release|x86.ActiveCfg = Release|Any CPU + {C2D3E4F5-6A7B-8C9D-0E1F-2A3B4C5D6E7F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -111,6 +139,8 @@ Global {E23C760E-B826-4B4F-BE76-916D86BAD2DB} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} {E41F0471-B14D-4FA0-9D8B-1E7750695AE9} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} {A54EE328-D456-4BAF-A180-84E77E6409AC} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} + {B1C3A2F4-5D6E-7F8A-9B0C-1D2E3F4A5B6C} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} + {C2D3E4F5-6A7B-8C9D-0E1F-2A3B4C5D6E7F} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5C159F93-800B-49E7-9905-EE09F8B8434A} diff --git a/src/System.CommandLine.StaticCompletions.Tests/BashShellProviderTests.cs b/src/System.CommandLine.StaticCompletions.Tests/BashShellProviderTests.cs new file mode 100644 index 0000000000..9e368a5fbf --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/BashShellProviderTests.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +namespace System.CommandLine.StaticCompletions.Tests; + +using System.CommandLine.StaticCompletions.Shells; + +public class BashShellProviderTests(ITestOutputHelper log) +{ + private IShellProvider provider = new BashShellProvider(); + [Fact] + public async Task GenericCompletions() + { + await provider.Verify(new("mycommand"), log); + } + + [Fact] + public async Task SimpleOptionCompletion() + { + await provider.Verify(new("mycommand") { + new Option("--name") + }, log); + } + + [Fact] + public async Task SubcommandAndOptionInTopLevelList() + { + await provider.Verify(new("mycommand") { + new Option("--name"), + new Command("subcommand") + }, log); + } + + [Fact] + public async Task NestedSubcommandCompletion() + { + await provider.Verify(new("mycommand") { + new Command("subcommand") { + new Command("nested") + } + }, log); + } +} diff --git a/src/System.CommandLine.StaticCompletions.Tests/Directory.Build.targets b/src/System.CommandLine.StaticCompletions.Tests/Directory.Build.targets new file mode 100644 index 0000000000..3d8551bb94 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/Directory.Build.targets @@ -0,0 +1,11 @@ + + + + + + + Library + + + diff --git a/src/System.CommandLine.StaticCompletions.Tests/GlobalUsings.cs b/src/System.CommandLine.StaticCompletions.Tests/GlobalUsings.cs new file mode 100644 index 0000000000..7ad692d8b7 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/GlobalUsings.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +global using Xunit; +global using Xunit.Abstractions; +global using FluentAssertions; diff --git a/src/System.CommandLine.StaticCompletions.Tests/HelpExtensionsTests.cs b/src/System.CommandLine.StaticCompletions.Tests/HelpExtensionsTests.cs new file mode 100644 index 0000000000..9cd8949bfb --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/HelpExtensionsTests.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +namespace System.CommandLine.StaticCompletions.Tests; + +using System.CommandLine.Help; +using System.CommandLine.StaticCompletions; + +public class HelpExtensionsTests +{ + [Fact] + public void HelpOptionOnlyShowsUsefulNames() + { + new HelpOption().Names().Should().BeEquivalentTo(["--help", "-h"]); + } + + [Fact] + public void OptionNamesListNameThenAliases() + { + new Option("--name", "-n", "--nombre").Names().Should().Equal(["--name", "-n", "--nombre"]); + } + + [Fact] + public void OptionsWithNoAliasesHaveOnlyOneName() + { + new Option("--name").Names().Should().Equal(["--name"]); + } + + [Fact] + public void HeirarchicalOptionsAreFlattened() + { + var parentCommand = new Command("parent"); + var childCommand = new Command("child"); + parentCommand.Subcommands.Add(childCommand); + parentCommand.Options.Add(new Option("--parent-global") { Recursive = true }); + parentCommand.Options.Add(new Option("--parent-local") { Recursive = false }); + parentCommand.Options.Add(new Option("--parent-global-but-hidden") { Recursive = true, Hidden = true }); + + childCommand.Options.Add(new Option("--child-local")); + childCommand.Options.Add(new Option("--child-hidden") { Hidden = true }); + + // note: no parent-local or parent-global-but-hidden options, and no locally hidden options + childCommand.HierarchicalOptions().Select(c => c.Name).Should().Equal(["--child-local", "--parent-global"]); + } +} diff --git a/src/System.CommandLine.StaticCompletions.Tests/PowershellProviderTests.cs b/src/System.CommandLine.StaticCompletions.Tests/PowershellProviderTests.cs new file mode 100644 index 0000000000..48c00bd72a --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/PowershellProviderTests.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +namespace System.CommandLine.StaticCompletions.Tests; + +using System.CommandLine.StaticCompletions.Shells; +using EmptyFiles; + +public class PowershellProviderTests(ITestOutputHelper log) +{ + private IShellProvider provider = new PowerShellShellProvider(); + + [Fact] + public async Task GenericCompletions() + { + await provider.Verify(new("mycommand"), log); + } + + [Fact] + public async Task SimpleOptionCompletion() + { + await provider.Verify(new("mycommand") { + new Option("--name") + }, log); + } + + [Fact] + public async Task SubcommandAndOptionInTopLevelList() + { + await provider.Verify(new("mycommand") { + new Option("--name"), + new Command("subcommand") + }, log); + } + + [Fact] + public async Task NestedSubcommandCompletion() + { + await provider.Verify(new("mycommand") { + new Command("subcommand") { + new Command("nested") + } + }, log); + } +} diff --git a/src/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj b/src/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj new file mode 100644 index 0000000000..b8f59c72f5 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj @@ -0,0 +1,24 @@ + + + $(NetMinimum) + enable + false + true + + + + + + + + + + + + + + + + + + diff --git a/src/System.CommandLine.StaticCompletions.Tests/VerifyConfiguration.cs b/src/System.CommandLine.StaticCompletions.Tests/VerifyConfiguration.cs new file mode 100644 index 0000000000..4eeca67880 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/VerifyConfiguration.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +namespace System.CommandLine.StaticCompletions.Tests; + +using System.Runtime.CompilerServices; + +public static class VerifyConfiguration +{ + [ModuleInitializer] + public static void Initialize() + { + VerifyDiffPlex.Initialize(VerifyTests.DiffPlex.OutputType.Compact); + + if (Environment.GetEnvironmentVariable("CI") is string ci && ci.Equals("true", StringComparison.OrdinalIgnoreCase)) + { + Verifier.DerivePathInfo((sourceFile, projectDirectory, type, method) => new( + directory: Path.Combine(Environment.CurrentDirectory, "snapshots"), + typeName: type.Name, + methodName: method.Name) + ); + } + + EmptyFiles.FileExtensions.AddTextExtension("ps1"); + EmptyFiles.FileExtensions.AddTextExtension("nu"); + } +} diff --git a/src/System.CommandLine.StaticCompletions.Tests/VerifyExtensions.cs b/src/System.CommandLine.StaticCompletions.Tests/VerifyExtensions.cs new file mode 100644 index 0000000000..35fd7c4dde --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/VerifyExtensions.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine.StaticCompletions.Shells; +using System.Runtime.CompilerServices; + +namespace System.CommandLine.StaticCompletions.Tests; + +public static class VerifyExtensions +{ + public static async Task Verify(this IShellProvider provider, Command command, ITestOutputHelper log, [CallerFilePath] string sourceFile = "") + { + var settings = new VerifySettings(); + settings.UseDirectory(Path.Combine("snapshots", provider.ArgumentName)); + var completions = provider.GenerateCompletions(command); + await Verifier.Verify(target: completions, extension: provider.Extension, settings: settings, sourceFile: sourceFile); + } +} diff --git a/src/System.CommandLine.StaticCompletions.Tests/ZshShellProviderTests.cs b/src/System.CommandLine.StaticCompletions.Tests/ZshShellProviderTests.cs new file mode 100644 index 0000000000..96bda10a9b --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/ZshShellProviderTests.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +namespace System.CommandLine.StaticCompletions.Tests; + +using System.CommandLine.Help; +using System.CommandLine.StaticCompletions.Shells; + +public class ZshShellProviderTests(ITestOutputHelper log) +{ + private IShellProvider _provider = new ZshShellProvider(); + + [Fact] + public async Task GenericCompletions() + { + Command command = new Command("my-app") { + new Option("-c") { + Arity = ArgumentArity.Zero, + Recursive = true + }, + new Option("-v") { + Arity = ArgumentArity.Zero + }, + new HelpOption(), + new Command("test", "Subcommand\nwith a second line") { + new Option("--debug", "-d") + { + Arity = ArgumentArity.Zero + } + }, + new Command("help", "Print this message or the help of the given subcommand(s)") { + new Command("test") + } + }; + await _provider.Verify(command, log); + } + + [Fact] + public async Task DynamicCompletionsGeneration() + { + var staticOption = new Option("--static") + { + IsDynamic = true + }; + staticOption.AcceptOnlyFromAmong("1", "2", "3"); + var dynamicArg = new Argument("--dynamic") + { + IsDynamic = true + }; + dynamicArg.CompletionSources.Add((context) => + { + return [ + new ("4"), + new ("5"), + new ("6") + ]; + }); + Command command = new Command("my-app") + { + staticOption, + dynamicArg + }; + await _provider.Verify(command, log); + } + + [Fact] + public async Task CustomStaticCompletionsGeneration() + { + var staticOption = new Option("--static"); + staticOption.AcceptOnlyFromAmong("1", "2", "3"); + var dynamicArg = new Argument("--dynamic"); + dynamicArg.CompletionSources.Add((context) => + { + return [ + new ("4"), + new ("5"), + new ("6") + ]; + }); + Command command = new Command("my-app") + { + staticOption, + dynamicArg + }; + await _provider.Verify(command, log); + } +} diff --git a/src/System.CommandLine.StaticCompletions.Tests/snapshots/bash/BashShellProviderTests.GenericCompletions.verified.sh b/src/System.CommandLine.StaticCompletions.Tests/snapshots/bash/BashShellProviderTests.GenericCompletions.verified.sh new file mode 100644 index 0000000000..f058e8a948 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/snapshots/bash/BashShellProviderTests.GenericCompletions.verified.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +_mycommand() { + + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + COMPREPLY=() + + opts="" + + if [[ $COMP_CWORD == "1" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} + + + +complete -F _mycommand mycommand \ No newline at end of file diff --git a/src/System.CommandLine.StaticCompletions.Tests/snapshots/bash/BashShellProviderTests.NestedSubcommandCompletion.verified.sh b/src/System.CommandLine.StaticCompletions.Tests/snapshots/bash/BashShellProviderTests.NestedSubcommandCompletion.verified.sh new file mode 100644 index 0000000000..944892b500 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/snapshots/bash/BashShellProviderTests.NestedSubcommandCompletion.verified.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +_mycommand() { + + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + COMPREPLY=() + + opts="subcommand" + + if [[ $COMP_CWORD == "1" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + + case ${COMP_WORDS[1]} in + (subcommand) + _mycommand_subcommand 2 + return + ;; + + esac + + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} + +_mycommand_subcommand() { + + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + COMPREPLY=() + + opts="nested" + + if [[ $COMP_CWORD == "$1" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + + case ${COMP_WORDS[$1]} in + (nested) + _mycommand_subcommand_nested $(($1+1)) + return + ;; + + esac + + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} + +_mycommand_subcommand_nested() { + + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + COMPREPLY=() + + opts="" + + if [[ $COMP_CWORD == "$1" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} + + + +complete -F _mycommand mycommand \ No newline at end of file diff --git a/src/System.CommandLine.StaticCompletions.Tests/snapshots/bash/BashShellProviderTests.SimpleOptionCompletion.verified.sh b/src/System.CommandLine.StaticCompletions.Tests/snapshots/bash/BashShellProviderTests.SimpleOptionCompletion.verified.sh new file mode 100644 index 0000000000..d8225ff8d3 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/snapshots/bash/BashShellProviderTests.SimpleOptionCompletion.verified.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +_mycommand() { + + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + COMPREPLY=() + + opts="--name" + + if [[ $COMP_CWORD == "1" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} + + + +complete -F _mycommand mycommand \ No newline at end of file diff --git a/src/System.CommandLine.StaticCompletions.Tests/snapshots/bash/BashShellProviderTests.SubcommandAndOptionInTopLevelList.verified.sh b/src/System.CommandLine.StaticCompletions.Tests/snapshots/bash/BashShellProviderTests.SubcommandAndOptionInTopLevelList.verified.sh new file mode 100644 index 0000000000..6816b8eeeb --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/snapshots/bash/BashShellProviderTests.SubcommandAndOptionInTopLevelList.verified.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +_mycommand() { + + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + COMPREPLY=() + + opts="subcommand --name" + + if [[ $COMP_CWORD == "1" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + + case ${COMP_WORDS[1]} in + (subcommand) + _mycommand_subcommand 2 + return + ;; + + esac + + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} + +_mycommand_subcommand() { + + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + COMPREPLY=() + + opts="" + + if [[ $COMP_CWORD == "$1" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} + + + +complete -F _mycommand mycommand \ No newline at end of file diff --git a/src/System.CommandLine.StaticCompletions.Tests/snapshots/pwsh/PowershellProviderTests.GenericCompletions.verified.ps1 b/src/System.CommandLine.StaticCompletions.Tests/snapshots/pwsh/PowershellProviderTests.GenericCompletions.verified.ps1 new file mode 100644 index 0000000000..b1431d93c5 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/snapshots/pwsh/PowershellProviderTests.GenericCompletions.verified.ps1 @@ -0,0 +1,31 @@ +using namespace System.Management.Automation +using namespace System.Management.Automation.Language + +Register-ArgumentCompleter -Native -CommandName 'mycommand' -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) + + $commandElements = $commandAst.CommandElements + $command = @( + 'mycommand' + for ($i = 1; $i -lt $commandElements.Count; $i++) { + $element = $commandElements[$i] + if ($element -isnot [StringConstantExpressionAst] -or + $element.StringConstantType -ne [StringConstantType]::BareWord -or + $element.Value.StartsWith('-') -or + $element.Value -eq $wordToComplete) { + break + } + $element.Value + }) -join ';' + + $completions = @() + switch ($command) { + 'mycommand' { + $staticCompletions = @( + ) + $completions += $staticCompletions + break + } + } + $completions | Where-Object -FilterScript { $_.CompletionText -like "$wordToComplete*" } | Sort-Object -Property ListItemText +} diff --git a/src/System.CommandLine.StaticCompletions.Tests/snapshots/pwsh/PowershellProviderTests.NestedSubcommandCompletion.verified.ps1 b/src/System.CommandLine.StaticCompletions.Tests/snapshots/pwsh/PowershellProviderTests.NestedSubcommandCompletion.verified.ps1 new file mode 100644 index 0000000000..94e7ce9e8e --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/snapshots/pwsh/PowershellProviderTests.NestedSubcommandCompletion.verified.ps1 @@ -0,0 +1,45 @@ +using namespace System.Management.Automation +using namespace System.Management.Automation.Language + +Register-ArgumentCompleter -Native -CommandName 'mycommand' -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) + + $commandElements = $commandAst.CommandElements + $command = @( + 'mycommand' + for ($i = 1; $i -lt $commandElements.Count; $i++) { + $element = $commandElements[$i] + if ($element -isnot [StringConstantExpressionAst] -or + $element.StringConstantType -ne [StringConstantType]::BareWord -or + $element.Value.StartsWith('-') -or + $element.Value -eq $wordToComplete) { + break + } + $element.Value + }) -join ';' + + $completions = @() + switch ($command) { + 'mycommand' { + $staticCompletions = @( + [CompletionResult]::new('subcommand', 'subcommand', [CompletionResultType]::ParameterValue, "subcommand") + ) + $completions += $staticCompletions + break + } + 'mycommand;subcommand' { + $staticCompletions = @( + [CompletionResult]::new('nested', 'nested', [CompletionResultType]::ParameterValue, "nested") + ) + $completions += $staticCompletions + break + } + 'mycommand;subcommand;nested' { + $staticCompletions = @( + ) + $completions += $staticCompletions + break + } + } + $completions | Where-Object -FilterScript { $_.CompletionText -like "$wordToComplete*" } | Sort-Object -Property ListItemText +} diff --git a/src/System.CommandLine.StaticCompletions.Tests/snapshots/pwsh/PowershellProviderTests.SimpleOptionCompletion.verified.ps1 b/src/System.CommandLine.StaticCompletions.Tests/snapshots/pwsh/PowershellProviderTests.SimpleOptionCompletion.verified.ps1 new file mode 100644 index 0000000000..fa6dc4ad99 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/snapshots/pwsh/PowershellProviderTests.SimpleOptionCompletion.verified.ps1 @@ -0,0 +1,32 @@ +using namespace System.Management.Automation +using namespace System.Management.Automation.Language + +Register-ArgumentCompleter -Native -CommandName 'mycommand' -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) + + $commandElements = $commandAst.CommandElements + $command = @( + 'mycommand' + for ($i = 1; $i -lt $commandElements.Count; $i++) { + $element = $commandElements[$i] + if ($element -isnot [StringConstantExpressionAst] -or + $element.StringConstantType -ne [StringConstantType]::BareWord -or + $element.Value.StartsWith('-') -or + $element.Value -eq $wordToComplete) { + break + } + $element.Value + }) -join ';' + + $completions = @() + switch ($command) { + 'mycommand' { + $staticCompletions = @( + [CompletionResult]::new('--name', '--name', [CompletionResultType]::ParameterName, "--name") + ) + $completions += $staticCompletions + break + } + } + $completions | Where-Object -FilterScript { $_.CompletionText -like "$wordToComplete*" } | Sort-Object -Property ListItemText +} diff --git a/src/System.CommandLine.StaticCompletions.Tests/snapshots/pwsh/PowershellProviderTests.SubcommandAndOptionInTopLevelList.verified.ps1 b/src/System.CommandLine.StaticCompletions.Tests/snapshots/pwsh/PowershellProviderTests.SubcommandAndOptionInTopLevelList.verified.ps1 new file mode 100644 index 0000000000..757523ec6c --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/snapshots/pwsh/PowershellProviderTests.SubcommandAndOptionInTopLevelList.verified.ps1 @@ -0,0 +1,39 @@ +using namespace System.Management.Automation +using namespace System.Management.Automation.Language + +Register-ArgumentCompleter -Native -CommandName 'mycommand' -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) + + $commandElements = $commandAst.CommandElements + $command = @( + 'mycommand' + for ($i = 1; $i -lt $commandElements.Count; $i++) { + $element = $commandElements[$i] + if ($element -isnot [StringConstantExpressionAst] -or + $element.StringConstantType -ne [StringConstantType]::BareWord -or + $element.Value.StartsWith('-') -or + $element.Value -eq $wordToComplete) { + break + } + $element.Value + }) -join ';' + + $completions = @() + switch ($command) { + 'mycommand' { + $staticCompletions = @( + [CompletionResult]::new('--name', '--name', [CompletionResultType]::ParameterName, "--name") + [CompletionResult]::new('subcommand', 'subcommand', [CompletionResultType]::ParameterValue, "subcommand") + ) + $completions += $staticCompletions + break + } + 'mycommand;subcommand' { + $staticCompletions = @( + ) + $completions += $staticCompletions + break + } + } + $completions | Where-Object -FilterScript { $_.CompletionText -like "$wordToComplete*" } | Sort-Object -Property ListItemText +} diff --git a/src/System.CommandLine.StaticCompletions.Tests/snapshots/zsh/ZshShellProviderTests.CustomStaticCompletionsGeneration.verified.zsh b/src/System.CommandLine.StaticCompletions.Tests/snapshots/zsh/ZshShellProviderTests.CustomStaticCompletionsGeneration.verified.zsh new file mode 100644 index 0000000000..13b8d0d007 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/snapshots/zsh/ZshShellProviderTests.CustomStaticCompletionsGeneration.verified.zsh @@ -0,0 +1,34 @@ +#compdef my-app + +autoload -U is-at-least + +_my-app() { + typeset -A opt_args + typeset -a _arguments_options + local ret=1 + + if is-at-least 5.2; then + _arguments_options=(-s -S -C) + else + _arguments_options=(-s -C) + fi + + local context curcontext="$curcontext" state state_descr line + _arguments "${_arguments_options[@]}" : \ + '--static=[]: :((1\:"1" 2\:"2" 3\:"3" ))' \ + ':--dynamic:((4\:"4" 5\:"5" 6\:"6" ))' \ + && ret=0 + local original_args="my-app ${line[@]}" +} + +(( $+functions[_my-app_commands] )) || +_my-app_commands() { + local commands; commands=() + _describe -t commands 'my-app commands' commands "$@" +} + +if [ "$funcstack[1]" = "_my-app" ]; then + _my-app "$@" +else + compdef _my-app my-app +fi diff --git a/src/System.CommandLine.StaticCompletions.Tests/snapshots/zsh/ZshShellProviderTests.DynamicCompletionsGeneration.verified.zsh b/src/System.CommandLine.StaticCompletions.Tests/snapshots/zsh/ZshShellProviderTests.DynamicCompletionsGeneration.verified.zsh new file mode 100644 index 0000000000..9312e626ce --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/snapshots/zsh/ZshShellProviderTests.DynamicCompletionsGeneration.verified.zsh @@ -0,0 +1,44 @@ +#compdef my-app + +autoload -U is-at-least + +_my-app() { + typeset -A opt_args + typeset -a _arguments_options + local ret=1 + + if is-at-least 5.2; then + _arguments_options=(-s -S -C) + else + _arguments_options=(-s -C) + fi + + local context curcontext="$curcontext" state state_descr line + _arguments "${_arguments_options[@]}" : \ + '--static=[]: :->dotnet_dynamic_complete' \ + ':--dynamic:->dotnet_dynamic_complete' \ + && ret=0 + case $state in + (dotnet_dynamic_complete) + local completions=() + local result=$(dotnet complete -- "${original_args[@]}") + for line in ${(f)result}; do + completions+=(${(q)line}) + done + _describe 'completions' $completions && ret=0 + ;; + esac + local original_args="my-app ${line[@]}" +} + +(( $+functions[_my-app_commands] )) || +_my-app_commands() { + local commands; commands=() + _describe -t commands 'my-app commands' commands "$@" +} + +if [ "$funcstack[1]" = "_my-app" ]; then + _my-app "$@" +else + compdef _my-app my-app +fi diff --git a/src/System.CommandLine.StaticCompletions.Tests/snapshots/zsh/ZshShellProviderTests.GenericCompletions.verified.zsh b/src/System.CommandLine.StaticCompletions.Tests/snapshots/zsh/ZshShellProviderTests.GenericCompletions.verified.zsh new file mode 100644 index 0000000000..01441fed29 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/snapshots/zsh/ZshShellProviderTests.GenericCompletions.verified.zsh @@ -0,0 +1,104 @@ +#compdef my-app + +autoload -U is-at-least + +_my-app() { + typeset -A opt_args + typeset -a _arguments_options + local ret=1 + + if is-at-least 5.2; then + _arguments_options=(-s -S -C) + else + _arguments_options=(-s -C) + fi + + local context curcontext="$curcontext" state state_descr line + _arguments "${_arguments_options[@]}" : \ + '-c[]' \ + '-v[]' \ + '--help[Show help and usage information]' \ + '-h[Show help and usage information]' \ + ":: :_my-app_commands" \ + "*::: :->my-app" \ + && ret=0 + local original_args="my-app ${line[@]}" + case $state in + (my-app) + words=($line[1] "${words[@]}") + (( CURRENT += 1 )) + curcontext="${curcontext%:*:*}:my-app-command-$line[1]:" + case $line[1] in + (test) + _arguments "${_arguments_options[@]}" : \ + '--debug[]' \ + '-d[]' \ + '-c[]' \ + '--help[Show help and usage information]' \ + '-h[Show help and usage information]' \ + && ret=0 + ;; + (help) + _arguments "${_arguments_options[@]}" : \ + '-c[]' \ + '--help[Show help and usage information]' \ + '-h[Show help and usage information]' \ + ":: :_my-app__help_commands" \ + "*::: :->help" \ + && ret=0 + case $state in + (help) + words=($line[1] "${words[@]}") + (( CURRENT += 1 )) + curcontext="${curcontext%:*:*}:my-app-help-command-$line[1]:" + case $line[1] in + (test) + _arguments "${_arguments_options[@]}" : \ + '-c[]' \ + '--help[Show help and usage information]' \ + '-h[Show help and usage information]' \ + && ret=0 + ;; + esac + ;; + esac + ;; + esac + ;; + esac +} + +(( $+functions[_my-app_commands] )) || +_my-app_commands() { + local commands; commands=( + 'test:Subcommand with a second line' \ + 'help:Print this message or the help of the given subcommand(s)' \ + ) + _describe -t commands 'my-app commands' commands "$@" +} + +(( $+functions[_my-app__test_commands] )) || +_my-app__test_commands() { + local commands; commands=() + _describe -t commands 'my-app test commands' commands "$@" +} + +(( $+functions[_my-app__help_commands] )) || +_my-app__help_commands() { + local commands; commands=( + 'test:' \ + ) + _describe -t commands 'my-app help commands' commands "$@" +} + +(( $+functions[_my-app__help__test_commands] )) || +_my-app__help__test_commands() { + local commands; commands=() + _describe -t commands 'my-app help test commands' commands "$@" +} + +if [ "$funcstack[1]" = "_my-app" ]; then + _my-app "$@" +else + compdef _my-app my-app +fi diff --git a/src/System.CommandLine.StaticCompletions/CompletionsCommandDefinition.cs b/src/System.CommandLine.StaticCompletions/CompletionsCommandDefinition.cs new file mode 100644 index 0000000000..2969e302e2 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions/CompletionsCommandDefinition.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine.StaticCompletions.Resources; + +namespace System.CommandLine.StaticCompletions; + +public sealed class CompletionsCommandDefinition : Command +{ + public readonly Argument ShellArgument = new("shell") + { + Description = Strings.CompletionsCommand_ShellArgument_Description, + Arity = ArgumentArity.ZeroOrOne, + DefaultValueFactory = _ => ShellNames.GetShellNameFromEnvironment() + }; + + public readonly CompletionsGenerateScriptCommandDefinition GenerateScriptCommand; + + public CompletionsCommandDefinition() + : base("completions", Strings.CompletionsCommand_Description) + { + Subcommands.Add(GenerateScriptCommand = new(this)); + + Validators.Add(argumentResult => + { + if (argumentResult.Tokens.Count == 0) + { + return; + } + + var singleToken = argumentResult.Tokens[0]; + if (!ShellNames.All.Contains(singleToken.Value)) + { + argumentResult.AddError(string.Format(Strings.ShellDiscovery_ShellNotSupported, singleToken.Value, string.Join(", ", ShellNames.All))); + } + }); + } +} diff --git a/src/System.CommandLine.StaticCompletions/CompletionsCommandParser.cs b/src/System.CommandLine.StaticCompletions/CompletionsCommandParser.cs new file mode 100644 index 0000000000..74d427f303 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions/CompletionsCommandParser.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine.Completions; +using System.CommandLine.StaticCompletions.Shells; +using System.Diagnostics; + +namespace System.CommandLine.StaticCompletions; + +public sealed class CompletionsCommandParser +{ + public static readonly IReadOnlyDictionary ShellProviders; + + static CompletionsCommandParser() + { + var providers = new IShellProvider[] + { + new BashShellProvider(), + new PowerShellShellProvider(), + new FishShellProvider(), + new ZshShellProvider(), + new NushellShellProvider() + }; + + Debug.Assert(providers.Select(provider => provider.ArgumentName).SequenceEqual(ShellNames.All)); + + ShellProviders = providers.ToDictionary(s => s.ArgumentName, StringComparer.OrdinalIgnoreCase); + } + + public static void ConfigureCommand(CompletionsCommandDefinition command) + { + command.ShellArgument.CompletionSources.Add(context => + ShellNames.All.Select(shellName => new CompletionItem(shellName, documentation: ShellProviders[shellName].HelpDescription))); + + command.GenerateScriptCommand.SetAction(args => + { + var shellName = args.GetValue(command.GenerateScriptCommand.ShellArgument) ?? throw new InvalidOperationException(); + var shell = ShellProviders[shellName]; + + var script = shell.GenerateCompletions(args.RootCommandResult.Command); + args.InvocationConfiguration.Output.Write(script); + }); + } +} diff --git a/src/System.CommandLine.StaticCompletions/CompletionsGenerateScriptCommandDefinition.cs b/src/System.CommandLine.StaticCompletions/CompletionsGenerateScriptCommandDefinition.cs new file mode 100644 index 0000000000..48f73a9fbe --- /dev/null +++ b/src/System.CommandLine.StaticCompletions/CompletionsGenerateScriptCommandDefinition.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine.StaticCompletions.Resources; + +namespace System.CommandLine.StaticCompletions; + +public sealed class CompletionsGenerateScriptCommandDefinition : Command +{ + public readonly Argument ShellArgument; + + public CompletionsGenerateScriptCommandDefinition(CompletionsCommandDefinition parent) + : base("script", Strings.GenerateCommand_Description) + { + Arguments.Add(ShellArgument = parent.ShellArgument); + } +} + diff --git a/src/System.CommandLine.StaticCompletions/DynamicSymbolExtensions.cs b/src/System.CommandLine.StaticCompletions/DynamicSymbolExtensions.cs new file mode 100644 index 0000000000..cd37cc1375 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions/DynamicSymbolExtensions.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.CommandLine.StaticCompletions; + +/// +/// Extensions for marking options or arguments require dynamic completions. Such symbols get special handling +/// in the static completion generation logic. +/// +public static class DynamicSymbolExtensions +{ + private static readonly object s_guard = new(); + + /// + /// The state that is used to track which symbols are dynamic. + /// + private static readonly Dictionary s_dynamicSymbols = []; + + extension(Option option) + { + /// + /// Indicates whether this option requires a dynamic call into the dotnet process to compute completions. + /// + public bool IsDynamic + { + get + { + lock (s_guard) + { + return s_dynamicSymbols.GetValueOrDefault(option, false); + } + } + set + { + lock (s_guard) + { + s_dynamicSymbols[option] = value; + } + } + } + } + + extension(Argument argument) + { + /// + /// Indicates whether this argument requires a dynamic call into the dotnet process to compute completions. + /// + public bool IsDynamic + { + get + { + lock (s_guard) + { + return s_dynamicSymbols.GetValueOrDefault(argument, false); + } + } + set + { + lock (s_guard) + { + s_dynamicSymbols[argument] = value; + } + } + } + } +} diff --git a/src/System.CommandLine.StaticCompletions/HelpGenerationExtensions.cs b/src/System.CommandLine.StaticCompletions/HelpGenerationExtensions.cs new file mode 100644 index 0000000000..193234dc60 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions/HelpGenerationExtensions.cs @@ -0,0 +1,115 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.CommandLine.StaticCompletions; + +using System.CommandLine; + +public static class HelpExtensions +{ + /// + /// Create a unique shell function name for a command - these names should be + /// * distinct from the 'root' command's name (i.e. we should not generate the function name 'dotnet' for the binary 'dotnet') + /// * distinct based on 'path' to get to this function (hence the parentCommandNames) + /// + /// + /// The chain of commands to get to this command + /// + public static string FunctionName(this Command command, string[]? parentCommandNames = null) => parentCommandNames switch + { + null => "_" + command.Name, + [] => "_" + command.Name, + var names => "_" + string.Join('_', names) + "_" + command.Name + }; + + /// + /// Sanitizes a function name to be safe for bash + /// + /// + /// + public static string MakeSafeFunctionName(this string functionName) => functionName.Replace('-', '_'); + + /// + /// Get all names for an option, including the primary name and all aliases + /// + /// + /// + public static string[] Names(this Option option) + { + var (primary, aliases) = PrimaryNameAndAliases(option); + return aliases is null ? [primary] : [primary, .. aliases]; + } + + public static (string primary, string[]? aliases) PrimaryNameAndAliases(this Option option) + { + if (option.Aliases.Count == 0) + { + return (option.Name, null); + } + else if (option is System.CommandLine.Help.HelpOption) // some of the help aliases are truly horrible + { + return ("--help", ["-h"]); + } + else + { + return (option.Name, [.. option.Aliases]); + } + } + + /// + /// Get all names for a command, including the primary name and all aliases + /// + /// + /// + public static string[] Names(this Command command) + { + if (command.Aliases.Count == 0) + { + return [command.Name]; + } + else + { + return [command.Name, .. command.Aliases]; + } + } + + public static IEnumerable - \ No newline at end of file + diff --git a/NuGet.config b/NuGet.config index 9f4127a7b2..bea52b0212 100644 --- a/NuGet.config +++ b/NuGet.config @@ -9,4 +9,8 @@ + + + + From 42aba6ef51c70a8617ccec21d86e4054ab3d29a2 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" Date: Wed, 1 Jul 2026 02:10:49 +0000 Subject: [PATCH 11/22] Update dependencies from build 320973 Updated Dependencies: Microsoft.DotNet.Arcade.Sdk (Version 11.0.0-beta.26329.111 -> 11.0.0-beta.26330.112) [[ commit created by automation ]] --- eng/Version.Details.props | 2 +- eng/Version.Details.xml | 6 +++--- global.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 19d4ae6b7f..185af22362 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,7 +6,7 @@ This file should be imported by eng/Versions.props - 11.0.0-beta.26329.111 + 11.0.0-beta.26330.112 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index a0e0412a82..95bdf74cda 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,12 +1,12 @@ - + - + https://github.com/dotnet/dotnet - 907cd6fc287a3613d7469246a57009b379c296e0 + a9679a8de87742e170318ea39f7f0e2874bcd441 diff --git a/global.json b/global.json index 6f96290b56..85218b42f9 100644 --- a/global.json +++ b/global.json @@ -13,6 +13,6 @@ } }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26329.111" + "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26330.112" } } From 358819c11f4e926da70f134690e717cea7e42ebe Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" Date: Thu, 2 Jul 2026 02:06:26 +0000 Subject: [PATCH 12/22] Update dependencies from build 321130 Updated Dependencies: Microsoft.DotNet.Arcade.Sdk (Version 11.0.0-beta.26330.112 -> 11.0.0-beta.26351.108) [[ commit created by automation ]] --- eng/Version.Details.props | 2 +- eng/Version.Details.xml | 6 +++--- eng/common/core-templates/job/helix-job-monitor.yml | 9 +++++++++ eng/common/tools.sh | 2 +- global.json | 2 +- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 185af22362..0819d45740 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,7 +6,7 @@ This file should be imported by eng/Versions.props - 11.0.0-beta.26330.112 + 11.0.0-beta.26351.108 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 95bdf74cda..248462cafe 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,12 +1,12 @@ - + - + https://github.com/dotnet/dotnet - a9679a8de87742e170318ea39f7f0e2874bcd441 + 8050375aad65a00d4e475163c0b91fe22573b6bf diff --git a/eng/common/core-templates/job/helix-job-monitor.yml b/eng/common/core-templates/job/helix-job-monitor.yml index a8162c5116..96287e55a1 100644 --- a/eng/common/core-templates/job/helix-job-monitor.yml +++ b/eng/common/core-templates/job/helix-job-monitor.yml @@ -57,6 +57,14 @@ parameters: type: number default: 30 +# When 'true' (the default), Helix work items that exit 0 but have failed AzDO test results +# are treated as failed: they count toward the monitor's exit code and are resubmitted by a +# later invocation's retry pass. Set to 'false' to fall back to exit-code-only outcomes. +# Forwarded as --fail-on-failed-tests. +- name: failWorkItemsWithFailedTests + type: boolean + default: true + # Advanced: optional pipeline artifact (produced earlier in this run) that contains the tool # nupkg. When set, the artifact is downloaded and the tool is installed from the nupkg into # a local tool-path; this bypasses the repo's .config/dotnet-tools.json manifest and is @@ -170,6 +178,7 @@ jobs: toolArgs=( --helix-base-uri '${{ parameters.helixBaseUri }}' --polling-interval-seconds '${{ parameters.pollingIntervalSeconds }}' + --fail-on-failed-tests '${{ parameters.failWorkItemsWithFailedTests }}' --max-wait-minutes "$((${{ parameters.timeoutInMinutes }} - 5))" # Set the tool's timeout slightly lower than the Azure DevOps job timeout to allow it to exit gracefully. --stage-name '$(System.StageName)' ) diff --git a/eng/common/tools.sh b/eng/common/tools.sh index 95aa8e7cc4..8d1a6b4610 100755 --- a/eng/common/tools.sh +++ b/eng/common/tools.sh @@ -423,7 +423,7 @@ function InitializeToolset { if [[ -z "$nuget_config" ]]; then # Search for any variation of nuget.config in the RepoRoot local found_config - found_config=$(find "$repo_root" -maxdepth 1 -type f -iname "nuget.config" -print -quit) + found_config=$(find "$repo_root" -maxdepth 1 -type f -iname nuget.config | head -n 1) if [[ -n "$found_config" ]]; then nuget_config="$found_config" diff --git a/global.json b/global.json index 85218b42f9..3826abce20 100644 --- a/global.json +++ b/global.json @@ -13,6 +13,6 @@ } }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26330.112" + "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26351.108" } } From 64bdd23d5d9b9262f395a3a758264106b379d1c9 Mon Sep 17 00:00:00 2001 From: Stuart Lang Date: Thu, 2 Jul 2026 22:44:02 +0100 Subject: [PATCH 13/22] Sync fish and nushell providers with dotnet/sdk; warn when [suggest] is disabled - Port the static+dynamic hybrid FishShellProvider from dotnet/sdk#53716, with the dynamic call invoking the [suggest] directive instead of a 'complete' subcommand (resolving the upstream TODO about the call being bound to the .NET CLI), plus its snapshot tests. - Port the nushell script fix from dotnet/sdk#54743 (export extern + nu-complete instead of the outdated external_completer config pattern), genericized for any binary via the [suggest] directive. - Warn on stderr when generating a script for an app that has dynamic completions but no [suggest] directive on its root command, since those parts of the script would silently do nothing. Co-Authored-By: Claude Fable 5 --- .../CompletionsCommandParserTests.cs | 76 ++++ .../FishShellProviderTests.cs | 118 ++++++ .../NushellShellProviderTests.cs | 19 + .../VerifyConfiguration.cs | 1 + ...ests.BoundedMultiValueOption.verified.fish | 67 +++ ...DynamicCompletionsGeneration.verified.fish | 52 +++ ...iderTests.GenericCompletions.verified.fish | 20 + ...viderTests.MixedArityOptions.verified.fish | 87 ++++ ...s.NestedSubcommandCompletion.verified.fish | 34 ++ ...Tests.SimpleOptionCompletion.verified.fish | 50 +++ ...iderTests.StaticOptionValues.verified.fish | 55 +++ ...mmandAndOptionInTopLevelList.verified.fish | 54 +++ ...ts.UnboundedMultiValueOption.verified.fish | 68 +++ ...oviderTests.GenericCompletions.verified.nu | 10 + .../CompletionsCommandParser.cs | 20 +- .../Resources/Strings.resx | 4 + .../Resources/xlf/Strings.cs.xlf | 5 + .../Resources/xlf/Strings.de.xlf | 5 + .../Resources/xlf/Strings.es.xlf | 5 + .../Resources/xlf/Strings.fr.xlf | 5 + .../Resources/xlf/Strings.it.xlf | 5 + .../Resources/xlf/Strings.ja.xlf | 5 + .../Resources/xlf/Strings.ko.xlf | 5 + .../Resources/xlf/Strings.pl.xlf | 5 + .../Resources/xlf/Strings.pt-BR.xlf | 5 + .../Resources/xlf/Strings.ru.xlf | 5 + .../Resources/xlf/Strings.tr.xlf | 5 + .../Resources/xlf/Strings.zh-Hans.xlf | 5 + .../Resources/xlf/Strings.zh-Hant.xlf | 5 + .../shells/FishShellProvider.cs | 397 +++++++++++++++++- .../shells/NuShellShellProvider.cs | 30 +- 31 files changed, 1190 insertions(+), 37 deletions(-) create mode 100644 src/System.CommandLine.StaticCompletions.Tests/CompletionsCommandParserTests.cs create mode 100644 src/System.CommandLine.StaticCompletions.Tests/FishShellProviderTests.cs create mode 100644 src/System.CommandLine.StaticCompletions.Tests/NushellShellProviderTests.cs create mode 100644 src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.BoundedMultiValueOption.verified.fish create mode 100644 src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.DynamicCompletionsGeneration.verified.fish create mode 100644 src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.GenericCompletions.verified.fish create mode 100644 src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.MixedArityOptions.verified.fish create mode 100644 src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.NestedSubcommandCompletion.verified.fish create mode 100644 src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.SimpleOptionCompletion.verified.fish create mode 100644 src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.StaticOptionValues.verified.fish create mode 100644 src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.SubcommandAndOptionInTopLevelList.verified.fish create mode 100644 src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.UnboundedMultiValueOption.verified.fish create mode 100644 src/System.CommandLine.StaticCompletions.Tests/snapshots/nushell/NushellShellProviderTests.GenericCompletions.verified.nu diff --git a/src/System.CommandLine.StaticCompletions.Tests/CompletionsCommandParserTests.cs b/src/System.CommandLine.StaticCompletions.Tests/CompletionsCommandParserTests.cs new file mode 100644 index 0000000000..9f56e9c4c0 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/CompletionsCommandParserTests.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +namespace System.CommandLine.StaticCompletions.Tests; + +using System.CommandLine.Completions; + +public class CompletionsCommandParserTests +{ + private static RootCommand CreateRootCommandWithCompletions(bool withDynamicSymbol) + { + var rootCommand = new RootCommand("my test app"); + if (withDynamicSymbol) + { + rootCommand.Options.Add(new Option("--name") { IsDynamic = true }); + } + + var completionsCommand = new CompletionsCommandDefinition(); + CompletionsCommandParser.ConfigureCommand(completionsCommand); + rootCommand.Subcommands.Add(completionsCommand); + return rootCommand; + } + + private static (int exitCode, string output, string error) GenerateScript(RootCommand rootCommand) + { + var output = new StringWriter(); + var error = new StringWriter(); + var exitCode = rootCommand.Parse(["completions", "script", "bash"]).Invoke(new InvocationConfiguration + { + Output = output, + Error = error + }); + return (exitCode, output.ToString(), error.ToString()); + } + + [Fact] + public void NoWarningWhenSuggestDirectiveIsEnabled() + { + var rootCommand = CreateRootCommandWithCompletions(withDynamicSymbol: true); + + var (exitCode, output, error) = GenerateScript(rootCommand); + + exitCode.Should().Be(0); + output.Should().NotBeEmpty(); + error.Should().BeEmpty(); + } + + [Fact] + public void WarnsWhenSuggestDirectiveIsDisabledAndDynamicSymbolsArePresent() + { + var rootCommand = CreateRootCommandWithCompletions(withDynamicSymbol: true); + rootCommand.Directives.Remove(rootCommand.Directives.OfType().Single()); + + var (exitCode, output, error) = GenerateScript(rootCommand); + + // the script is still generated, but the user is warned that dynamic completions won't resolve + exitCode.Should().Be(0); + output.Should().NotBeEmpty(); + error.Should().Contain("[suggest]"); + } + + [Fact] + public void NoWarningWhenSuggestDirectiveIsDisabledButNoDynamicSymbolsArePresent() + { + var rootCommand = CreateRootCommandWithCompletions(withDynamicSymbol: false); + rootCommand.Directives.Remove(rootCommand.Directives.OfType().Single()); + + var (exitCode, output, error) = GenerateScript(rootCommand); + + exitCode.Should().Be(0); + output.Should().NotBeEmpty(); + error.Should().BeEmpty(); + } +} diff --git a/src/System.CommandLine.StaticCompletions.Tests/FishShellProviderTests.cs b/src/System.CommandLine.StaticCompletions.Tests/FishShellProviderTests.cs new file mode 100644 index 0000000000..464b796034 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/FishShellProviderTests.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +namespace System.CommandLine.StaticCompletions.Tests; + +using System.CommandLine.StaticCompletions.Shells; + +public class FishShellProviderTests(ITestOutputHelper log) +{ + private IShellProvider provider = new FishShellProvider(); + + [Fact] + public async Task GenericCompletions() + { + await provider.Verify(new("mycommand"), log); + } + + [Fact] + public async Task SimpleOptionCompletion() + { + await provider.Verify(new("mycommand") { + new Option("--name") + }, log); + } + + [Fact] + public async Task SubcommandAndOptionInTopLevelList() + { + await provider.Verify(new("mycommand") { + new Option("--name"), + new Command("subcommand") + }, log); + } + + [Fact] + public async Task NestedSubcommandCompletion() + { + await provider.Verify(new("mycommand") { + new Command("subcommand") { + new Command("nested") + } + }, log); + } + + [Fact] + public async Task DynamicCompletionsGeneration() + { + var dynamicOption = new Option("--name") { IsDynamic = true }; + var dynamicArg = new Argument("target") { IsDynamic = true }; + await provider.Verify(new("mycommand") { dynamicOption, dynamicArg }, log); + } + + [Fact] + public async Task StaticOptionValues() + { + var staticOption = new Option("--verbosity"); + staticOption.AcceptOnlyFromAmong("quiet", "minimal", "normal", "detailed", "diagnostic"); + await provider.Verify(new("mycommand") { + staticOption + }, log); + } + + [Fact] + public async Task BoundedMultiValueOption() + { + var multiOption = new Option("--sources") + { + Arity = new ArgumentArity(1, 3) + }; + multiOption.AcceptOnlyFromAmong("src1", "src2", "src3", "src4"); + await provider.Verify(new("mycommand") { + multiOption, + new Command("subcommand") + }, log); + } + + [Fact] + public async Task UnboundedMultiValueOption() + { + var unboundedOption = new Option("--items") + { + Arity = ArgumentArity.ZeroOrMore + }; + unboundedOption.AcceptOnlyFromAmong("a", "b", "c"); + await provider.Verify(new("mycommand") { + unboundedOption, + new Option("--name"), + new Command("subcommand") + }, log); + } + + [Fact] + public async Task MixedArityOptions() + { + var singleOption = new Option("--config"); + singleOption.AcceptOnlyFromAmong("debug", "release"); + + var multiOption = new Option("--framework", "-f") + { + Arity = new ArgumentArity(1, 3) + }; + multiOption.AcceptOnlyFromAmong("net8.0", "net9.0", "net10.0"); + + var unboundedOption = new Option("--sources") + { + Arity = ArgumentArity.OneOrMore + }; + + await provider.Verify(new("mycommand") { + singleOption, + multiOption, + unboundedOption, + new Command("build") + }, log); + } +} diff --git a/src/System.CommandLine.StaticCompletions.Tests/NushellShellProviderTests.cs b/src/System.CommandLine.StaticCompletions.Tests/NushellShellProviderTests.cs new file mode 100644 index 0000000000..87b141350b --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/NushellShellProviderTests.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +namespace System.CommandLine.StaticCompletions.Tests; + +using System.CommandLine.StaticCompletions.Shells; + +public class NushellShellProviderTests(ITestOutputHelper log) +{ + private IShellProvider provider = new NushellShellProvider(); + + [Fact] + public async Task GenericCompletions() + { + await provider.Verify(new("mycommand"), log); + } +} diff --git a/src/System.CommandLine.StaticCompletions.Tests/VerifyConfiguration.cs b/src/System.CommandLine.StaticCompletions.Tests/VerifyConfiguration.cs index 4eeca67880..71dd56b1d2 100644 --- a/src/System.CommandLine.StaticCompletions.Tests/VerifyConfiguration.cs +++ b/src/System.CommandLine.StaticCompletions.Tests/VerifyConfiguration.cs @@ -25,5 +25,6 @@ public static void Initialize() EmptyFiles.FileExtensions.AddTextExtension("ps1"); EmptyFiles.FileExtensions.AddTextExtension("nu"); + EmptyFiles.FileExtensions.AddTextExtension("fish"); } } diff --git a/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.BoundedMultiValueOption.verified.fish b/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.BoundedMultiValueOption.verified.fish new file mode 100644 index 0000000000..b33eebee90 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.BoundedMultiValueOption.verified.fish @@ -0,0 +1,67 @@ +# fish completions for mycommand + +function _mycommand + set -l tokens (commandline -opc) + + set -l state 0 + set -l i 2 + while test $i -le (count $tokens) + set -l word $tokens[$i] + switch $state + case 0 + switch $word + case subcommand + set state 1 + case --sources + set -l skip_max 3 + set -l skipped 0 + while test $skipped -lt $skip_max -a (math $i + 1) -le (count $tokens) + set -l next $tokens[(math $i + 1)] + if string match -q -- '-*' $next + break + end + set i (math $i + 1) + set skipped (math $skipped + 1) + end + end + end + set i (math $i + 1) + end + + set -l opt_index 0 + if test (count $tokens) -ge 2 + for j in (seq (count $tokens) -1 2) + if string match -q -- '-*' $tokens[$j] + set opt_index $j + break + end + end + end + + if test $opt_index -gt 0 + set -l opt $tokens[$opt_index] + set -l values_after (math (count $tokens) - $opt_index) + switch $state + case 0 + switch $opt + case --sources + if test $values_after -lt 3 + printf '%s\n' 'src1' + printf '%s\n' 'src2' + printf '%s\n' 'src3' + printf '%s\n' 'src4' + return + end + end + end + end + + switch $state + case 0 + printf '%s\n' 'subcommand' + printf '%s\n' '--sources' + case 1 + end +end + +complete -c mycommand -f -a '(_mycommand)' diff --git a/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.DynamicCompletionsGeneration.verified.fish b/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.DynamicCompletionsGeneration.verified.fish new file mode 100644 index 0000000000..abdc1e9431 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.DynamicCompletionsGeneration.verified.fish @@ -0,0 +1,52 @@ +# fish completions for mycommand + +function _mycommand + set -l tokens (commandline -opc) + + set -l state 0 + set -l i 2 + while test $i -le (count $tokens) + set -l word $tokens[$i] + switch $state + case 0 + switch $word + case --name + set i (math $i + 1) + end + end + set i (math $i + 1) + end + + set -l opt_index 0 + if test (count $tokens) -ge 2 + for j in (seq (count $tokens) -1 2) + if string match -q -- '-*' $tokens[$j] + set opt_index $j + break + end + end + end + + if test $opt_index -gt 0 + set -l opt $tokens[$opt_index] + set -l values_after (math (count $tokens) - $opt_index) + switch $state + case 0 + switch $opt + case --name + if test $values_after -lt 1 + command $tokens[1] "[suggest:"(commandline -C)"]" (commandline -cp) 2>/dev/null + return + end + end + end + end + + switch $state + case 0 + printf '%s\n' '--name' + command $tokens[1] "[suggest:"(commandline -C)"]" (commandline -cp) 2>/dev/null + end +end + +complete -c mycommand -f -a '(_mycommand)' diff --git a/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.GenericCompletions.verified.fish b/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.GenericCompletions.verified.fish new file mode 100644 index 0000000000..c86386e997 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.GenericCompletions.verified.fish @@ -0,0 +1,20 @@ +# fish completions for mycommand + +function _mycommand + set -l tokens (commandline -opc) + + set -l state 0 + set -l i 2 + while test $i -le (count $tokens) + set -l word $tokens[$i] + switch $state + end + set i (math $i + 1) + end + + switch $state + case 0 + end +end + +complete -c mycommand -f -a '(_mycommand)' diff --git a/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.MixedArityOptions.verified.fish b/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.MixedArityOptions.verified.fish new file mode 100644 index 0000000000..a5728a17ce --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.MixedArityOptions.verified.fish @@ -0,0 +1,87 @@ +# fish completions for mycommand + +function _mycommand + set -l tokens (commandline -opc) + + set -l state 0 + set -l i 2 + while test $i -le (count $tokens) + set -l word $tokens[$i] + switch $state + case 0 + switch $word + case build + set state 1 + case --config + set i (math $i + 1) + case --framework -f + set -l skip_max 3 + set -l skipped 0 + while test $skipped -lt $skip_max -a (math $i + 1) -le (count $tokens) + set -l next $tokens[(math $i + 1)] + if string match -q -- '-*' $next + break + end + set i (math $i + 1) + set skipped (math $skipped + 1) + end + case --sources + while test (math $i + 1) -le (count $tokens) + set -l next $tokens[(math $i + 1)] + if string match -q -- '-*' $next + break + end + set i (math $i + 1) + end + end + end + set i (math $i + 1) + end + + set -l opt_index 0 + if test (count $tokens) -ge 2 + for j in (seq (count $tokens) -1 2) + if string match -q -- '-*' $tokens[$j] + set opt_index $j + break + end + end + end + + if test $opt_index -gt 0 + set -l opt $tokens[$opt_index] + set -l values_after (math (count $tokens) - $opt_index) + switch $state + case 0 + switch $opt + case --config + if test $values_after -lt 1 + printf '%s\n' 'debug' + printf '%s\n' 'release' + return + end + case --framework -f + if test $values_after -lt 3 + printf '%s\n' 'net10.0' + printf '%s\n' 'net8.0' + printf '%s\n' 'net9.0' + return + end + case --sources + return + end + end + end + + switch $state + case 0 + printf '%s\n' 'build' + printf '%s\n' '--config' + printf '%s\n' '--framework' + printf '%s\n' '-f' + printf '%s\n' '--sources' + case 1 + end +end + +complete -c mycommand -f -a '(_mycommand)' diff --git a/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.NestedSubcommandCompletion.verified.fish b/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.NestedSubcommandCompletion.verified.fish new file mode 100644 index 0000000000..400d43cf29 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.NestedSubcommandCompletion.verified.fish @@ -0,0 +1,34 @@ +# fish completions for mycommand + +function _mycommand + set -l tokens (commandline -opc) + + set -l state 0 + set -l i 2 + while test $i -le (count $tokens) + set -l word $tokens[$i] + switch $state + case 0 + switch $word + case subcommand + set state 1 + end + case 1 + switch $word + case nested + set state 2 + end + end + set i (math $i + 1) + end + + switch $state + case 0 + printf '%s\n' 'subcommand' + case 1 + printf '%s\n' 'nested' + case 2 + end +end + +complete -c mycommand -f -a '(_mycommand)' diff --git a/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.SimpleOptionCompletion.verified.fish b/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.SimpleOptionCompletion.verified.fish new file mode 100644 index 0000000000..68d8ab21e6 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.SimpleOptionCompletion.verified.fish @@ -0,0 +1,50 @@ +# fish completions for mycommand + +function _mycommand + set -l tokens (commandline -opc) + + set -l state 0 + set -l i 2 + while test $i -le (count $tokens) + set -l word $tokens[$i] + switch $state + case 0 + switch $word + case --name + set i (math $i + 1) + end + end + set i (math $i + 1) + end + + set -l opt_index 0 + if test (count $tokens) -ge 2 + for j in (seq (count $tokens) -1 2) + if string match -q -- '-*' $tokens[$j] + set opt_index $j + break + end + end + end + + if test $opt_index -gt 0 + set -l opt $tokens[$opt_index] + set -l values_after (math (count $tokens) - $opt_index) + switch $state + case 0 + switch $opt + case --name + if test $values_after -lt 1 + return + end + end + end + end + + switch $state + case 0 + printf '%s\n' '--name' + end +end + +complete -c mycommand -f -a '(_mycommand)' diff --git a/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.StaticOptionValues.verified.fish b/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.StaticOptionValues.verified.fish new file mode 100644 index 0000000000..98a413a449 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.StaticOptionValues.verified.fish @@ -0,0 +1,55 @@ +# fish completions for mycommand + +function _mycommand + set -l tokens (commandline -opc) + + set -l state 0 + set -l i 2 + while test $i -le (count $tokens) + set -l word $tokens[$i] + switch $state + case 0 + switch $word + case --verbosity + set i (math $i + 1) + end + end + set i (math $i + 1) + end + + set -l opt_index 0 + if test (count $tokens) -ge 2 + for j in (seq (count $tokens) -1 2) + if string match -q -- '-*' $tokens[$j] + set opt_index $j + break + end + end + end + + if test $opt_index -gt 0 + set -l opt $tokens[$opt_index] + set -l values_after (math (count $tokens) - $opt_index) + switch $state + case 0 + switch $opt + case --verbosity + if test $values_after -lt 1 + printf '%s\n' 'detailed' + printf '%s\n' 'diagnostic' + printf '%s\n' 'minimal' + printf '%s\n' 'normal' + printf '%s\n' 'quiet' + return + end + end + end + end + + switch $state + case 0 + printf '%s\n' '--verbosity' + end +end + +complete -c mycommand -f -a '(_mycommand)' diff --git a/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.SubcommandAndOptionInTopLevelList.verified.fish b/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.SubcommandAndOptionInTopLevelList.verified.fish new file mode 100644 index 0000000000..0936b3b28c --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.SubcommandAndOptionInTopLevelList.verified.fish @@ -0,0 +1,54 @@ +# fish completions for mycommand + +function _mycommand + set -l tokens (commandline -opc) + + set -l state 0 + set -l i 2 + while test $i -le (count $tokens) + set -l word $tokens[$i] + switch $state + case 0 + switch $word + case subcommand + set state 1 + case --name + set i (math $i + 1) + end + end + set i (math $i + 1) + end + + set -l opt_index 0 + if test (count $tokens) -ge 2 + for j in (seq (count $tokens) -1 2) + if string match -q -- '-*' $tokens[$j] + set opt_index $j + break + end + end + end + + if test $opt_index -gt 0 + set -l opt $tokens[$opt_index] + set -l values_after (math (count $tokens) - $opt_index) + switch $state + case 0 + switch $opt + case --name + if test $values_after -lt 1 + return + end + end + end + end + + switch $state + case 0 + printf '%s\n' 'subcommand' + printf '%s\n' '--name' + case 1 + end +end + +complete -c mycommand -f -a '(_mycommand)' diff --git a/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.UnboundedMultiValueOption.verified.fish b/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.UnboundedMultiValueOption.verified.fish new file mode 100644 index 0000000000..92f17b003b --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.UnboundedMultiValueOption.verified.fish @@ -0,0 +1,68 @@ +# fish completions for mycommand + +function _mycommand + set -l tokens (commandline -opc) + + set -l state 0 + set -l i 2 + while test $i -le (count $tokens) + set -l word $tokens[$i] + switch $state + case 0 + switch $word + case subcommand + set state 1 + case --name + set i (math $i + 1) + case --items + while test (math $i + 1) -le (count $tokens) + set -l next $tokens[(math $i + 1)] + if string match -q -- '-*' $next + break + end + set i (math $i + 1) + end + end + end + set i (math $i + 1) + end + + set -l opt_index 0 + if test (count $tokens) -ge 2 + for j in (seq (count $tokens) -1 2) + if string match -q -- '-*' $tokens[$j] + set opt_index $j + break + end + end + end + + if test $opt_index -gt 0 + set -l opt $tokens[$opt_index] + set -l values_after (math (count $tokens) - $opt_index) + switch $state + case 0 + switch $opt + case --items + printf '%s\n' 'a' + printf '%s\n' 'b' + printf '%s\n' 'c' + return + case --name + if test $values_after -lt 1 + return + end + end + end + end + + switch $state + case 0 + printf '%s\n' 'subcommand' + printf '%s\n' '--items' + printf '%s\n' '--name' + case 1 + end +end + +complete -c mycommand -f -a '(_mycommand)' diff --git a/src/System.CommandLine.StaticCompletions.Tests/snapshots/nushell/NushellShellProviderTests.GenericCompletions.verified.nu b/src/System.CommandLine.StaticCompletions.Tests/snapshots/nushell/NushellShellProviderTests.GenericCompletions.verified.nu new file mode 100644 index 0000000000..6f382adacc --- /dev/null +++ b/src/System.CommandLine.StaticCompletions.Tests/snapshots/nushell/NushellShellProviderTests.GenericCompletions.verified.nu @@ -0,0 +1,10 @@ +# nushell completions for mycommand +# save this file and `source` it from your nushell config + +def "nu-complete mycommand" [context: string] { + ^mycommand $"[suggest:($context | str length)]" $context | lines +} + +export extern "mycommand" [ + ...command: string@"nu-complete mycommand" +] \ No newline at end of file diff --git a/src/System.CommandLine.StaticCompletions/CompletionsCommandParser.cs b/src/System.CommandLine.StaticCompletions/CompletionsCommandParser.cs index 74d427f303..522b088fac 100644 --- a/src/System.CommandLine.StaticCompletions/CompletionsCommandParser.cs +++ b/src/System.CommandLine.StaticCompletions/CompletionsCommandParser.cs @@ -37,8 +37,26 @@ public static void ConfigureCommand(CompletionsCommandDefinition command) var shellName = args.GetValue(command.GenerateScriptCommand.ShellArgument) ?? throw new InvalidOperationException(); var shell = ShellProviders[shellName]; - var script = shell.GenerateCompletions(args.RootCommandResult.Command); + var rootCommand = args.RootCommandResult.Command; + + // the generated scripts resolve dynamic completions by calling back into the application + // via the [suggest] directive - if that directive has been removed from the root command, + // those parts of the script will silently do nothing, so let the user know at generation time. + if (HasDynamicSymbols(rootCommand) && !HasSuggestDirective(rootCommand)) + { + args.InvocationConfiguration.Error.WriteLine(Resources.Strings.GenerateCommand_SuggestDirectiveDisabledWarning); + } + + var script = shell.GenerateCompletions(rootCommand); args.InvocationConfiguration.Output.Write(script); }); } + + private static bool HasSuggestDirective(Command command) => + command is RootCommand root && root.Directives.OfType().Any(); + + private static bool HasDynamicSymbols(Command command) => + command.Options.Any(o => !o.Hidden && o.IsDynamic) || + command.Arguments.Any(a => !a.Hidden && a.IsDynamic) || + command.Subcommands.Where(c => !c.Hidden).Any(HasDynamicSymbols); } diff --git a/src/System.CommandLine.StaticCompletions/Resources/Strings.resx b/src/System.CommandLine.StaticCompletions/Resources/Strings.resx index fccc7bd634..0e65176365 100644 --- a/src/System.CommandLine.StaticCompletions/Resources/Strings.resx +++ b/src/System.CommandLine.StaticCompletions/Resources/Strings.resx @@ -133,6 +133,10 @@ Generate the completion script for a supported shell + + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + {Locked="[suggest]"} + Generates a completion script for the Bourne Again SHell (bash). {Locked="Bourne Again SHell (bash)"} diff --git a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.cs.xlf b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.cs.xlf index e2a5294d6c..e81e59d3de 100644 --- a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.cs.xlf +++ b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.cs.xlf @@ -27,6 +27,11 @@ Vygenerovat skript pro dokončování pro podporované prostředí + + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + {Locked="[suggest]"} + Generates a completion script for the NuShell shell. Vygeneruje skript pro dokončování pro prostředí NuShell. diff --git a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.de.xlf b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.de.xlf index 17af23abea..a397f7dadf 100644 --- a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.de.xlf +++ b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.de.xlf @@ -27,6 +27,11 @@ Generieren Sie das Vervollständigungsskript für eine unterstützte Shell + + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + {Locked="[suggest]"} + Generates a completion script for the NuShell shell. Generiert ein Vervollständigungsskript für die NuShell Shell. diff --git a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.es.xlf b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.es.xlf index 4992812583..03abd69506 100644 --- a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.es.xlf +++ b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.es.xlf @@ -27,6 +27,11 @@ Generación del script de finalización para un shell compatible + + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + {Locked="[suggest]"} + Generates a completion script for the NuShell shell. Genera un script de finalización para el shell de NuShell. diff --git a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.fr.xlf b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.fr.xlf index ef4173dfb6..7b01601705 100644 --- a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.fr.xlf +++ b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.fr.xlf @@ -27,6 +27,11 @@ Générez le script d’achèvement pour un shell pris en charge + + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + {Locked="[suggest]"} + Generates a completion script for the NuShell shell. Génère un script d’achèvement pour l’interpréteur de commandes NuShell. diff --git a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.it.xlf b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.it.xlf index 1ced1b438a..0cc1662d42 100644 --- a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.it.xlf +++ b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.it.xlf @@ -27,6 +27,11 @@ Genera lo script di completamento per una shell supportata + + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + {Locked="[suggest]"} + Generates a completion script for the NuShell shell. Genera uno script di completamento per la shell NuShell. diff --git a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.ja.xlf b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.ja.xlf index bdf7e812f4..5d5334590e 100644 --- a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.ja.xlf +++ b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.ja.xlf @@ -27,6 +27,11 @@ サポートされているシェルの入力候補スクリプトを生成します + + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + {Locked="[suggest]"} + Generates a completion script for the NuShell shell. NuShell シェルの入力候補スクリプトを生成します。 diff --git a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.ko.xlf b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.ko.xlf index 8d1ce4e717..dc490aa6fe 100644 --- a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.ko.xlf +++ b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.ko.xlf @@ -27,6 +27,11 @@ 지원되는 셸에 대한 완료 스크립트 생성 + + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + {Locked="[suggest]"} + Generates a completion script for the NuShell shell. NuShell 셸에 대한 완성 스크립트를 생성합니다. diff --git a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.pl.xlf b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.pl.xlf index e736264a09..e3890d546d 100644 --- a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.pl.xlf +++ b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.pl.xlf @@ -27,6 +27,11 @@ Generowanie skryptu uzupełniania dla obsługiwanej powłoki + + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + {Locked="[suggest]"} + Generates a completion script for the NuShell shell. Generuje skrypt uzupełniania dla powłoki NuShell. diff --git a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.pt-BR.xlf b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.pt-BR.xlf index e3d17652e5..21215aa4f8 100644 --- a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.pt-BR.xlf +++ b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.pt-BR.xlf @@ -27,6 +27,11 @@ Gere o script de conclusão para um shell suportado + + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + {Locked="[suggest]"} + Generates a completion script for the NuShell shell. Gera um script de conclusão para o shell NuShell. diff --git a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.ru.xlf b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.ru.xlf index 2439701eda..a4923269af 100644 --- a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.ru.xlf +++ b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.ru.xlf @@ -27,6 +27,11 @@ Создать сценарий завершения для поддерживаемой оболочки + + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + {Locked="[suggest]"} + Generates a completion script for the NuShell shell. Создает сценарий завершения для оболочки NuShell. diff --git a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.tr.xlf b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.tr.xlf index 67edded115..e3613d0c94 100644 --- a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.tr.xlf +++ b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.tr.xlf @@ -27,6 +27,11 @@ Desteklenen bir kabuk için tamamlama betiği oluştur + + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + {Locked="[suggest]"} + Generates a completion script for the NuShell shell. NuShell kabuğu için bir tamamlama betiği oluşturur. diff --git a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.zh-Hans.xlf b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.zh-Hans.xlf index e2f12a89e6..1ab4b5f259 100644 --- a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.zh-Hans.xlf @@ -27,6 +27,11 @@ 生成受支持的 shell 的完成脚本 + + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + {Locked="[suggest]"} + Generates a completion script for the NuShell shell. 生成 NuShell shell 的完成脚本。 diff --git a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.zh-Hant.xlf b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.zh-Hant.xlf index 2bf90e808d..2c25019fa5 100644 --- a/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/System.CommandLine.StaticCompletions/Resources/xlf/Strings.zh-Hant.xlf @@ -27,6 +27,11 @@ 為支援的殼層產生完成指令碼 + + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + warning: this application has completions that are resolved dynamically, but the [suggest] directive is not enabled on the root command. Dynamic completions will not work in the generated script. + {Locked="[suggest]"} + Generates a completion script for the NuShell shell. 產生 NuShell 殼層的完成指令碼。 diff --git a/src/System.CommandLine.StaticCompletions/shells/FishShellProvider.cs b/src/System.CommandLine.StaticCompletions/shells/FishShellProvider.cs index 1f2f02c0b4..4c70bc2b41 100644 --- a/src/System.CommandLine.StaticCompletions/shells/FishShellProvider.cs +++ b/src/System.CommandLine.StaticCompletions/shells/FishShellProvider.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.CodeDom.Compiler; +using System.CommandLine.Completions; using System.CommandLine.StaticCompletions.Resources; namespace System.CommandLine.StaticCompletions.Shells; @@ -16,21 +18,388 @@ public class FishShellProvider : IShellProvider // override the ToString method to return the argument name so that CLI help is cleaner for 'default' values public override string ToString() => ArgumentName; - public string GenerateCompletions(System.CommandLine.Command command) + public string GenerateCompletions(Command command) { - var binary = command.Name; - var safeName = binary.Replace('-', '_').Replace('.', '_'); - return - $$""" - # fish parameter completion for {{binary}} - # add the following to your config.fish to enable completions + var safeName = command.Name.MakeSafeFunctionName(); - function __{{safeName}}_complete - set -l pos (commandline -C) - set -l line (commandline -cp) - {{binary}} "[suggest:$pos]" $line 2>/dev/null - end - complete -f -c {{binary}} -a '(__{{safeName}}_complete)' - """; + using var textWriter = new StringWriter { NewLine = "\n" }; + using var writer = new IndentedTextWriter(textWriter); + + writer.WriteLine($"# fish completions for {command.Name}"); + writer.WriteLine(); + + // Collect all commands in a flat list and assign numeric state IDs + var states = new List<(int id, Command cmd)>(); + CollectStates(command, states); + var stateIdByCommand = states.ToDictionary(s => s.cmd, s => s.id); + + // Write the main completion function + writer.WriteLine($"function _{safeName}"); + writer.Indent++; + + WriteTokenization(writer); + writer.WriteLine(); + WriteStateWalker(writer, states, stateIdByCommand); + writer.WriteLine(); + WriteOptionValueCompletions(writer, states); + WriteStateCompletions(writer, states); + + writer.Indent--; + writer.WriteLine("end"); + writer.WriteLine(); + + writer.WriteLine($"complete -c {command.Name} -f -a '(_{safeName})'"); + + writer.Flush(); + return textWriter.ToString(); + } + + /// + /// Recursively collect all visible commands into a flat list with numeric state IDs. + /// State 0 is the root command. + /// + private static void CollectStates(Command cmd, List<(int id, Command cmd)> states) + { + states.Add((states.Count, cmd)); + foreach (var sub in cmd.Subcommands.Where(c => !c.Hidden)) + { + CollectStates(sub, states); + } + } + + /// + /// Write the command line tokenization logic. + /// Uses fish's commandline builtin to get completed tokens up to the cursor. + /// + private static void WriteTokenization(IndentedTextWriter writer) + { + // -opc: tokenize, cut at cursor, only completed tokens (excludes current partial word) + writer.WriteLine("set -l tokens (commandline -opc)"); + } + + // Options with MaximumNumberOfValues at or above this threshold are treated as unbounded + // (i.e. consume tokens until an option-like token is encountered). + // System.CommandLine uses 100_000 as its internal sentinel for ZeroOrMore/OneOrMore. + private const int UnboundedArityThreshold = 100_000; + + /// + /// Generate the state machine that walks completed tokens to determine which subcommand context we're in. + /// For each state, we check if the current word matches a known subcommand (transitioning to that subcommand's state) + /// or a value-taking option (skipping tokens for the option's value(s), respecting the option's arity). + /// + private static void WriteStateWalker(IndentedTextWriter writer, List<(int id, Command cmd)> states, Dictionary stateIdByCommand) + { + writer.WriteLine("set -l state 0"); + writer.WriteLine("set -l i 2"); // start after the command name (fish arrays are 1-based) + writer.WriteLine("while test $i -le (count $tokens)"); + writer.Indent++; + writer.WriteLine("set -l word $tokens[$i]"); + + writer.WriteLine("switch $state"); + writer.Indent++; + + foreach (var (stateId, cmd) in states) + { + var visibleSubs = cmd.Subcommands.Where(c => !c.Hidden).ToArray(); + var valueOptions = cmd.HierarchicalOptions() + .Where(o => !o.Hidden && !o.IsFlag()) + .ToArray(); + + // Skip states that have no transitions to emit + if (visibleSubs.Length == 0 && valueOptions.Length == 0) + continue; + + writer.WriteLine($"case {stateId}"); + writer.Indent++; + writer.WriteLine("switch $word"); + writer.Indent++; + + // Subcommand transitions + foreach (var sub in visibleSubs) + { + var subStateId = stateIdByCommand[sub]; + var names = string.Join(" ", sub.Names()); + writer.WriteLine($"case {names}"); + writer.Indent++; + writer.WriteLine($"set state {subStateId}"); + writer.Indent--; + } + + // Single-value options (arity exactly 1): skip the next token + var singleValueNames = valueOptions + .Where(o => o.Arity.MaximumNumberOfValues == 1) + .SelectMany(o => o.Names()) + .ToArray(); + if (singleValueNames.Length > 0) + { + writer.WriteLine($"case {string.Join(" ", singleValueNames)}"); + writer.Indent++; + writer.WriteLine("set i (math $i + 1)"); + writer.Indent--; + } + + // Multi-value options (arity > 1): skip up to N tokens, stopping at option-like tokens. + // Group by arity so options with the same max can share a case branch. + var multiValueByArity = valueOptions + .Where(o => o.Arity.MaximumNumberOfValues > 1) + .GroupBy(o => o.Arity.MaximumNumberOfValues); + foreach (var group in multiValueByArity) + { + var names = string.Join(" ", group.SelectMany(o => o.Names())); + writer.WriteLine($"case {names}"); + writer.Indent++; + WriteMultiValueSkipLoop(writer, group.Key); + writer.Indent--; + } + + writer.Indent--; + writer.WriteLine("end"); + writer.Indent--; + } + + writer.Indent--; + writer.WriteLine("end"); + + writer.WriteLine("set i (math $i + 1)"); + writer.Indent--; + writer.WriteLine("end"); } + + /// + /// Emit a fish loop that skips value tokens after a multi-value option. + /// Stops when it has consumed maxValues tokens, or encounters a token starting with '-', + /// whichever comes first. For unbounded arities, only the '-' check applies. + /// + private static void WriteMultiValueSkipLoop(IndentedTextWriter writer, int maxValues) + { + bool isBounded = maxValues < UnboundedArityThreshold; + + if (isBounded) + { + writer.WriteLine($"set -l skip_max {maxValues}"); + writer.WriteLine("set -l skipped 0"); + writer.WriteLine("while test $skipped -lt $skip_max -a (math $i + 1) -le (count $tokens)"); + } + else + { + writer.WriteLine("while test (math $i + 1) -le (count $tokens)"); + } + + writer.Indent++; + writer.WriteLine("set -l next $tokens[(math $i + 1)]"); + writer.WriteLine("if string match -q -- '-*' $next"); + writer.Indent++; + writer.WriteLine("break"); + writer.Indent--; + writer.WriteLine("end"); + writer.WriteLine("set i (math $i + 1)"); + if (isBounded) + { + writer.WriteLine("set skipped (math $skipped + 1)"); + } + writer.Indent--; + writer.WriteLine("end"); + } + + /// + /// Generate option value completions. + /// Scans backward through completed tokens to find the nearest option, then checks whether + /// we're still within that option's arity. This correctly handles both single-value and + /// multi-value options (e.g. --sources foo bar with arity 3 still offers completions + /// for the third value). + /// + private static void WriteOptionValueCompletions(IndentedTextWriter writer, List<(int id, Command cmd)> states) + { + var hasAnyValueOptions = states.Any(s => + s.cmd.HierarchicalOptions().Any(o => !o.Hidden && !o.IsFlag())); + + if (!hasAnyValueOptions) return; + + // Scan backward through tokens to find the nearest option (token starting with -) + writer.WriteLine("set -l opt_index 0"); + writer.WriteLine("if test (count $tokens) -ge 2"); + writer.Indent++; + writer.WriteLine("for j in (seq (count $tokens) -1 2)"); + writer.Indent++; + writer.WriteLine("if string match -q -- '-*' $tokens[$j]"); + writer.Indent++; + writer.WriteLine("set opt_index $j"); + writer.WriteLine("break"); + writer.Indent--; + writer.WriteLine("end"); + writer.Indent--; + writer.WriteLine("end"); + writer.Indent--; + writer.WriteLine("end"); + writer.WriteLine(); + + writer.WriteLine("if test $opt_index -gt 0"); + writer.Indent++; + writer.WriteLine("set -l opt $tokens[$opt_index]"); + // values_after = number of non-option tokens between the option and the cursor + writer.WriteLine("set -l values_after (math (count $tokens) - $opt_index)"); + writer.WriteLine("switch $state"); + writer.Indent++; + + foreach (var (stateId, cmd) in states) + { + var valueOptions = cmd.HierarchicalOptions() + .Where(o => !o.Hidden && !o.IsFlag()) + .ToArray(); + + if (valueOptions.Length == 0) + continue; + + writer.WriteLine($"case {stateId}"); + writer.Indent++; + writer.WriteLine("switch $opt"); + writer.Indent++; + + foreach (var option in valueOptions) + { + var names = string.Join(" ", option.Names()); + var maxValues = option.Arity.MaximumNumberOfValues; + bool isBounded = maxValues < UnboundedArityThreshold; + + writer.WriteLine($"case {names}"); + writer.Indent++; + + // For bounded options, check that we haven't exceeded the arity. + // For unbounded options, always offer completions (any number of values is valid). + if (isBounded) + { + writer.WriteLine($"if test $values_after -lt {maxValues}"); + writer.Indent++; + } + + if (option.IsDynamic) + { + writer.WriteLine(GenerateDynamicCall()); + } + else + { + var completions = option.GetCompletions(CompletionContext.Empty).ToArray(); + foreach (var c in completions) + { + WriteCompletionCandidate(writer, c); + } + } + writer.WriteLine("return"); + + if (isBounded) + { + writer.Indent--; + writer.WriteLine("end"); + } + + writer.Indent--; + } + + writer.Indent--; + writer.WriteLine("end"); + writer.Indent--; + } + + writer.Indent--; + writer.WriteLine("end"); + writer.Indent--; + writer.WriteLine("end"); + writer.WriteLine(); + } + + /// + /// Generate the main completion output for each state. + /// Emits subcommands, options, and positional argument completions for the current context. + /// + private static void WriteStateCompletions(IndentedTextWriter writer, List<(int id, Command cmd)> states) + { + writer.WriteLine("switch $state"); + writer.Indent++; + + foreach (var (stateId, cmd) in states) + { + writer.WriteLine($"case {stateId}"); + writer.Indent++; + + // Subcommand completions + foreach (var sub in cmd.Subcommands.Where(c => !c.Hidden)) + { + WriteCandidate(writer, sub.Name, SanitizeDescription(sub.Description)); + } + + // Option completions - emit all aliases so both -h and --help are completable + foreach (var option in cmd.HierarchicalOptions().Where(o => !o.Hidden)) + { + var desc = SanitizeDescription(option.Description); + foreach (var name in option.Names()) + { + WriteCandidate(writer, name, desc); + } + } + + // Positional argument completions + foreach (var arg in cmd.Arguments.Where(a => !a.Hidden)) + { + if (arg.IsDynamic) + { + writer.WriteLine(GenerateDynamicCall()); + } + else + { + var completions = arg.GetCompletions(CompletionContext.Empty).ToArray(); + foreach (var c in completions) + { + WriteCompletionCandidate(writer, c); + } + } + } + + writer.Indent--; + } + + writer.Indent--; + writer.WriteLine("end"); + } + + /// + /// Write a single completion candidate line with an optional description. + /// Fish uses tab-separated format: candidate\tdescription + /// + private static void WriteCompletionCandidate(IndentedTextWriter writer, CompletionItem completion) + { + var label = completion.InsertText ?? completion.Label; + var desc = completion.Documentation ?? completion.Detail; + WriteCandidate(writer, label, desc); + } + + private static void WriteCandidate(IndentedTextWriter writer, string label, string? description) + { + if (!string.IsNullOrEmpty(description)) + writer.WriteLine($"printf '%s\\t%s\\n' {FishEscape(label)} {FishEscape(description)}"); + else + writer.WriteLine($"printf '%s\\n' {FishEscape(label)}"); + } + + /// + /// Generate a dynamic completion call that invokes the application's [suggest] directive + /// to resolve dynamic completions. + /// Uses the actual command from the command line ($tokens[1]) so it works regardless of how the binary was invoked. + /// + internal static string GenerateDynamicCall() + { + return """command $tokens[1] "[suggest:"(commandline -C)"]" (commandline -cp) 2>/dev/null"""; + } + + /// + /// Escape a string for use in a fish single-quoted string. + /// Fish single-quoted strings support \' and \\ as escape sequences. + /// + internal static string FishEscape(string? s) + { + if (string.IsNullOrEmpty(s)) return "''"; + return "'" + s.Replace("\\", "\\\\").Replace("'", "\\'") + "'"; + } + + private static string SanitizeDescription(string? s) => + s?.Replace("\r\n", " ").Replace('\n', ' ').Replace('\r', ' ').Replace('\t', ' ') ?? ""; } diff --git a/src/System.CommandLine.StaticCompletions/shells/NuShellShellProvider.cs b/src/System.CommandLine.StaticCompletions/shells/NuShellShellProvider.cs index 072ee9be18..ff8cc916e6 100644 --- a/src/System.CommandLine.StaticCompletions/shells/NuShellShellProvider.cs +++ b/src/System.CommandLine.StaticCompletions/shells/NuShellShellProvider.cs @@ -21,30 +21,16 @@ public string GenerateCompletions(System.CommandLine.Command command) var binary = command.Name; return $$""" - # Add the following content to your config.nu file: - - let external_completer = { |spans| - { - "{{binary}}": { || - let line = ($spans | skip 1 | str join " ") - {{binary}} $"[suggest:($line | str length)]" $line | lines - } - } | get $spans.0 | each { || do $in } - } + # nushell completions for {{binary}} + # save this file and `source` it from your nushell config - # And then in the config record, find the completions section and add the - # external_completer that was defined earlier to external: - - $env.config = { - # your options here - completions: { - # your options here - external: { - # your options here - completer: $external_completer # add it here - } - } + def "nu-complete {{binary}}" [context: string] { + ^{{binary}} $"[suggest:($context | str length)]" $context | lines } + + export extern "{{binary}}" [ + ...command: string@"nu-complete {{binary}}" + ] """; } } From 7a0168b981638eaee8d2f68d7a67e2c4340bea8f Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" Date: Fri, 3 Jul 2026 02:09:31 +0000 Subject: [PATCH 14/22] Update dependencies from build 321265 Updated Dependencies: Microsoft.DotNet.Arcade.Sdk (Version 11.0.0-beta.26351.108 -> 11.0.0-beta.26352.103) [[ commit created by automation ]] --- eng/Version.Details.props | 2 +- eng/Version.Details.xml | 6 +++--- global.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 0819d45740..98399fbe6b 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,7 +6,7 @@ This file should be imported by eng/Versions.props - 11.0.0-beta.26351.108 + 11.0.0-beta.26352.103 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 248462cafe..31e048f68c 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,12 +1,12 @@ - + - + https://github.com/dotnet/dotnet - 8050375aad65a00d4e475163c0b91fe22573b6bf + bcaa8d44d57494224d443dba7c1dfed381ad5528 diff --git a/global.json b/global.json index 3826abce20..d9644ff54d 100644 --- a/global.json +++ b/global.json @@ -13,6 +13,6 @@ } }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26351.108" + "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26352.103" } } From 2749c607af41055dc5f8aa021e5c50828418f3fc Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" Date: Sat, 4 Jul 2026 02:08:33 +0000 Subject: [PATCH 15/22] Update dependencies from build 321416 Updated Dependencies: Microsoft.DotNet.Arcade.Sdk (Version 11.0.0-beta.26352.103 -> 11.0.0-beta.26353.105) [[ commit created by automation ]] --- eng/Version.Details.props | 2 +- eng/Version.Details.xml | 6 +++--- global.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 98399fbe6b..a0baf39b6d 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,7 +6,7 @@ This file should be imported by eng/Versions.props - 11.0.0-beta.26352.103 + 11.0.0-beta.26353.105 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 31e048f68c..249157fa8c 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,12 +1,12 @@ - + - + https://github.com/dotnet/dotnet - bcaa8d44d57494224d443dba7c1dfed381ad5528 + 02385e26e452b21a97d8a5e694fe3ada7259a582 diff --git a/global.json b/global.json index d9644ff54d..9c9acf6c0d 100644 --- a/global.json +++ b/global.json @@ -13,6 +13,6 @@ } }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26352.103" + "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26353.105" } } From d78a4b184a0d0d31c1a46d7e9f613e7039d8d35f Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" Date: Mon, 6 Jul 2026 02:06:56 +0000 Subject: [PATCH 16/22] Update dependencies from build 321469 Updated Dependencies: Microsoft.DotNet.Arcade.Sdk (Version 11.0.0-beta.26353.105 -> 11.0.0-beta.26355.102) [[ commit created by automation ]] --- eng/Version.Details.props | 2 +- eng/Version.Details.xml | 6 +++--- global.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index a0baf39b6d..f30760db11 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,7 +6,7 @@ This file should be imported by eng/Versions.props - 11.0.0-beta.26353.105 + 11.0.0-beta.26355.102 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 249157fa8c..b24670ef22 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,12 +1,12 @@ - + - + https://github.com/dotnet/dotnet - 02385e26e452b21a97d8a5e694fe3ada7259a582 + b4b350a66ea5dcf13420747036d8b263cdf6cbef diff --git a/global.json b/global.json index 9c9acf6c0d..5468e7f891 100644 --- a/global.json +++ b/global.json @@ -13,6 +13,6 @@ } }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26353.105" + "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26355.102" } } From 1d9c5372bcbca2185a45f694ad494ce19b128416 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Mon, 6 Jul 2026 13:34:02 -0500 Subject: [PATCH 17/22] Update src/System.CommandLine.StaticCompletions/System.CommandLine.StaticCompletions.csproj Co-authored-by: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com> --- .../System.CommandLine.StaticCompletions.csproj | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/System.CommandLine.StaticCompletions/System.CommandLine.StaticCompletions.csproj b/src/System.CommandLine.StaticCompletions/System.CommandLine.StaticCompletions.csproj index ab600fa2ad..e4ef27816f 100644 --- a/src/System.CommandLine.StaticCompletions/System.CommandLine.StaticCompletions.csproj +++ b/src/System.CommandLine.StaticCompletions/System.CommandLine.StaticCompletions.csproj @@ -18,16 +18,7 @@ - - True - True - Strings.resx - - - MSBuild:_GenerateResxSource - Strings.Designer.cs - true - + From 08f4e837adc1477f5b521bee22fe96470a2bad77 Mon Sep 17 00:00:00 2001 From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com> Date: Mon, 6 Jul 2026 21:19:43 +0200 Subject: [PATCH 18/22] Follow-up feedback from recently merged PR --- .../Directory.Build.targets | 11 ----------- .../System.CommandLine.StaticCompletions.Tests.csproj | 3 +-- .../System.CommandLine.StaticCompletions.csproj | 8 ++------ .../System.CommandLine.Tests.csproj | 2 +- 4 files changed, 4 insertions(+), 20 deletions(-) delete mode 100644 src/System.CommandLine.StaticCompletions.Tests/Directory.Build.targets diff --git a/src/System.CommandLine.StaticCompletions.Tests/Directory.Build.targets b/src/System.CommandLine.StaticCompletions.Tests/Directory.Build.targets deleted file mode 100644 index 3d8551bb94..0000000000 --- a/src/System.CommandLine.StaticCompletions.Tests/Directory.Build.targets +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - Library - - - diff --git a/src/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj b/src/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj index b8f59c72f5..7843087772 100644 --- a/src/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj +++ b/src/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj @@ -1,9 +1,8 @@ + $(NetMinimum) enable - false - true diff --git a/src/System.CommandLine.StaticCompletions/System.CommandLine.StaticCompletions.csproj b/src/System.CommandLine.StaticCompletions/System.CommandLine.StaticCompletions.csproj index e4ef27816f..10177a9d2a 100644 --- a/src/System.CommandLine.StaticCompletions/System.CommandLine.StaticCompletions.csproj +++ b/src/System.CommandLine.StaticCompletions/System.CommandLine.StaticCompletions.csproj @@ -1,16 +1,12 @@ + $(NetMinimum) true enable enable true - - - true + true diff --git a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj index 89d4e8b865..d832598d27 100644 --- a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj +++ b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj @@ -17,7 +17,7 @@ - + From 4d416678b58cb7fd35fd04a11923cb7f0ebba0ee Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Mon, 6 Jul 2026 15:11:45 -0500 Subject: [PATCH 19/22] Add README for System.CommandLine.StaticCompletions package Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../README.md | 85 +++++++++++++++++++ ...ystem.CommandLine.StaticCompletions.csproj | 6 ++ 2 files changed, 91 insertions(+) create mode 100644 src/System.CommandLine.StaticCompletions/README.md diff --git a/src/System.CommandLine.StaticCompletions/README.md b/src/System.CommandLine.StaticCompletions/README.md new file mode 100644 index 0000000000..fe7e171223 --- /dev/null +++ b/src/System.CommandLine.StaticCompletions/README.md @@ -0,0 +1,85 @@ +# System.CommandLine.StaticCompletions + +System.CommandLine.StaticCompletions generates *static* shell completion scripts from a [System.CommandLine](https://www.nuget.org/packages/System.CommandLine) `Command` tree. It walks your command hierarchy - commands, subcommands, options, arguments, and aliases - and emits a native completion script for the user's shell. + +These scripts are "static" because they describe the shape of your CLI ahead of time and can be shipped, checked in, or installed by a package manager. Unlike the default `dotnet-suggest` integration, they don't require a separate global tool to be installed to provide completions for the fixed parts of your CLI. Where you genuinely need runtime values (for example, completing a name looked up from a database), individual symbols can opt into *dynamic* completions that call back into your application. + +Supported shells: + +* Bash (`bash`) +* Zsh (`zsh`) +* PowerShell / PowerShell Core (`pwsh`) +* Fish (`fish`) +* Nushell (`nushell`) + +## Getting Started + +Add a `completions` command to your application's root command. The `CompletionsCommandDefinition` provides the `completions script ` command, and `CompletionsCommandParser.ConfigureCommand` wires up the action that produces the script: + +```csharp +using System.CommandLine; +using System.CommandLine.StaticCompletions; + +RootCommand rootCommand = new("My application"); + +// ... add your options, arguments, and subcommands ... + +// Add the `completions` command to your CLI. +CompletionsCommandDefinition completionsCommand = new(); +CompletionsCommandParser.ConfigureCommand(completionsCommand); +rootCommand.Subcommands.Add(completionsCommand); + +return rootCommand.Parse(args).Invoke(); +``` + +Your users can now generate a completion script for their shell: + +```shell +# Generate a script for a specific shell +> myapp completions script bash > ~/.local/share/bash-completion/completions/myapp + +# The shell argument is optional - it defaults to the current shell +> myapp completions script >> $PROFILE +``` + +When the shell argument is omitted, the shell is detected from the environment (the `SHELL` variable on Unix, PowerShell on Windows). + +## Dynamic Completions + +Static scripts are perfect for the fixed structure of your CLI, but some completions can only be computed at runtime. Mark an option or argument as dynamic and the generated script will call back into your application (via System.CommandLine's built-in `[suggest]` directive) to resolve those values on demand: + +```csharp +using System.CommandLine; +using System.CommandLine.StaticCompletions; + +Option nameOption = new("--name") +{ + Description = "A name resolved at completion time" +}; + +// Values will be resolved by invoking the app at completion time. +nameOption.IsDynamic = true; +nameOption.CompletionSources.Add(_ => GetNamesFromDatabase()); + +rootCommand.Options.Add(nameOption); +``` + +> **Note:** Dynamic completions rely on the `[suggest]` directive being present on your root command (it is enabled by default). If you have removed it while dynamic symbols are present, script generation will emit a warning because those completions would silently do nothing. + +## Framework Support + +* **.NET 8.0+** - Trimming and AOT compatible. + +## License + +This package is licensed under the [MIT License](https://opensource.org/licenses/MIT). + +## Documentation + +* **[Microsoft Learn Documentation](https://learn.microsoft.com/en-us/dotnet/standard/commandline/)** - Guides and API reference for System.CommandLine. +* **[GitHub Repository](https://github.com/dotnet/command-line-api)** - Source code, samples, and issues. + +## Support + +* **Issues**: [GitHub Issues](https://github.com/dotnet/command-line-api/issues) +* **Discussions**: [GitHub Discussions](https://github.com/dotnet/command-line-api/discussions) diff --git a/src/System.CommandLine.StaticCompletions/System.CommandLine.StaticCompletions.csproj b/src/System.CommandLine.StaticCompletions/System.CommandLine.StaticCompletions.csproj index 10177a9d2a..cabd691944 100644 --- a/src/System.CommandLine.StaticCompletions/System.CommandLine.StaticCompletions.csproj +++ b/src/System.CommandLine.StaticCompletions/System.CommandLine.StaticCompletions.csproj @@ -5,14 +5,20 @@ true enable enable + Generates static shell completion scripts (bash, zsh, pwsh, fish, nushell) from a System.CommandLine command tree. true true + README.md + + + + From 85b7db9e5580c401069d3b060ca39a699c2af71c Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Mon, 6 Jul 2026 16:25:11 -0500 Subject: [PATCH 20/22] Update src/System.CommandLine.StaticCompletions/README.md Co-authored-by: Noah Gilson --- src/System.CommandLine.StaticCompletions/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.CommandLine.StaticCompletions/README.md b/src/System.CommandLine.StaticCompletions/README.md index fe7e171223..c0ac5bc485 100644 --- a/src/System.CommandLine.StaticCompletions/README.md +++ b/src/System.CommandLine.StaticCompletions/README.md @@ -76,7 +76,7 @@ This package is licensed under the [MIT License](https://opensource.org/licenses ## Documentation -* **[Microsoft Learn Documentation](https://learn.microsoft.com/en-us/dotnet/standard/commandline/)** - Guides and API reference for System.CommandLine. +* **[Microsoft Learn Documentation](https://learn.microsoft.com/dotnet/standard/commandline/)** - Guides and API reference for System.CommandLine. * **[GitHub Repository](https://github.com/dotnet/command-line-api)** - Source code, samples, and issues. ## Support From d423c6a490d5e228891555b7df30bf9ac7f1b528 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" Date: Tue, 7 Jul 2026 02:09:24 +0000 Subject: [PATCH 21/22] Update dependencies from build 321546 Updated Dependencies: Microsoft.DotNet.Arcade.Sdk (Version 11.0.0-beta.26355.102 -> 11.0.0-beta.26356.103) [[ commit created by automation ]] --- eng/Version.Details.props | 2 +- eng/Version.Details.xml | 6 +++--- global.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index f30760db11..b1be1bd3c8 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,7 +6,7 @@ This file should be imported by eng/Versions.props - 11.0.0-beta.26355.102 + 11.0.0-beta.26356.103 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index b24670ef22..3b3bb669ab 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,12 +1,12 @@ - + - + https://github.com/dotnet/dotnet - b4b350a66ea5dcf13420747036d8b263cdf6cbef + a67d575054ff3bcb905ea6699734d0ed8816aa2b diff --git a/global.json b/global.json index 5468e7f891..ae4153aec2 100644 --- a/global.json +++ b/global.json @@ -13,6 +13,6 @@ } }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26355.102" + "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26356.103" } } From 75e39ef2139253394a9f701f8ab5c0b30586d769 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Wed, 8 Jul 2026 09:14:33 -0500 Subject: [PATCH 22/22] Expect NativeAOT lib prefix on Unix for .NET 11 SDK Starting with the .NET 11 SDK, NativeAOT applies the \lib\ prefix by default to native library outputs on Unix (dotnet/docs#52324). This repo pins an 11.x SDK via global.json, so the published shared library is libNativeLibrary.so / libNativeLibrary.dylib. Update the test's expected filename accordingly so the file is found on Linux/macOS CI. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/System.CommandLine.Tests/CompilationTests.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/System.CommandLine.Tests/CompilationTests.cs b/src/System.CommandLine.Tests/CompilationTests.cs index cdc4d8dff9..102088a69a 100644 --- a/src/System.CommandLine.Tests/CompilationTests.cs +++ b/src/System.CommandLine.Tests/CompilationTests.cs @@ -121,8 +121,11 @@ public void RootCommand_name_falls_back_to_the_AppContext_value_when_hosted_as_a private static string NativeLibraryFileName(string assemblyName) => OperatingSystem.IsWindows() ? $"{assemblyName}.dll" - : OperatingSystem.IsMacOS() ? $"{assemblyName}.dylib" - : $"{assemblyName}.so"; + // NativeAOT applies the "lib" prefix to native library outputs on Unix by + // default starting with the .NET 11 SDK (see dotnet/docs#52324). This repo + // pins an 11.x SDK via global.json, so the prefix is always present. + : OperatingSystem.IsMacOS() ? $"lib{assemblyName}.dylib" + : $"lib{assemblyName}.so"; [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate int GetExecutableNameDelegate(IntPtr buffer, int bufferLength);