Skip to content

Commit af2fa73

Browse files
committed
Add tests for shell completion helpers
1 parent 3a72f9e commit af2fa73

File tree

3 files changed

+88
-35
lines changed

3 files changed

+88
-35
lines changed

src/Cli/dotnet/commands/dotnet-completions/shells/Bash.cs

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ private string GenerateCommandsCompletions(string[] parentCommandNames, CliComma
3636
// generate the words for options and subcommands
3737
var visibleSubcommands = command.Subcommands.Where(c => !c.Hidden).ToArray();
3838
// notably, do not generate completions for all option aliases - since a user is tab-completing we can use the longest forms
39-
var completionOptions = AllOptionsForCommand(command).Where(o => !o.Hidden).Select(o => o.Name).ToArray();
39+
var completionOptions = command.HeirarchicalOptions().Where(o => !o.Hidden).Select(o => o.Name).ToArray();
4040
var completionSubcommands = visibleSubcommands.Select(x => x.Name).ToArray();
4141
string[] completionWords = [.. completionSubcommands, .. completionOptions];
4242

@@ -122,35 +122,7 @@ private string GenerateCommandsCompletions(string[] parentCommandNames, CliComma
122122
return textWriter.ToString() + string.Join('\n', visibleSubcommands.Select(c => GenerateCommandsCompletions(parentCommandNamesForSubcommands, c, isNestedCommand: true)));
123123
}
124124

125-
private static IEnumerable<CliOption> AllOptionsForCommand(CliCommand c)
126-
{
127-
if (c.Parents.Count() == 0)
128-
{
129-
return c.Options;
130-
}
131-
else
132-
{
133-
return c.Options.Concat(c.Parents.OfType<CliCommand>().SelectMany(OptionsForParent));
134-
}
135-
136-
}
137-
138-
private static IEnumerable<CliOption> OptionsForParent(CliCommand c)
139-
{
140-
foreach (var o in c.Options)
141-
{
142-
if (o.Recursive) yield return o;
143-
}
144-
foreach (var p in c.Parents.OfType<CliCommand>())
145-
{
146-
foreach (var o in OptionsForParent(p))
147-
{
148-
yield return o;
149-
}
150-
}
151-
}
152-
153-
private static string[] PositionalArgumentTerms(CliArgument[] arguments)
125+
internal static string[] PositionalArgumentTerms(CliArgument[] arguments)
154126
{
155127
var completions = new List<string>();
156128
foreach (var argument in arguments)
@@ -176,12 +148,12 @@ private static string[] PositionalArgumentTerms(CliArgument[] arguments)
176148
/// Generates a call to `dotnet complete <string> --position <int>` for dynamic completions where necessary, but in a more generic way
177149
/// </summary>
178150
/// <returns></returns>
179-
private static string GenerateDynamicCall()
151+
internal static string GenerateDynamicCall()
180152
{
181153
return $$"""${COMP_WORDS[0]} complete --position ${COMP_POINT} ${COMP_LINE} 2>/dev/null | tr '\n' ' '""";
182154
}
183155

184-
private static string? GenerateOptionHandlers(CliCommand command)
156+
internal static string? GenerateOptionHandlers(CliCommand command)
185157
{
186158
var optionHandlers = command.Options.Where(o => !o.Hidden).Select(GenerateOptionHandler).Where(handler => handler is not null).ToArray();
187159
if (optionHandlers.Length == 0)
@@ -199,7 +171,7 @@ private static string GenerateDynamicCall()
199171
/// * a concrete set of choices in a bash array already ($opts), or
200172
/// * a subprocess that will return such an array (aka '(dotnet complete --position 10 'dotnet ad')') </param>
201173
/// <returns></returns>
202-
private static string GenerateChoicesPrompt(string choicesInvocation) => $$"""COMPREPLY=( $(compgen -W "{{choicesInvocation}}" -- "$cur") )""";
174+
internal static string GenerateChoicesPrompt(string choicesInvocation) => $$"""COMPREPLY=( $(compgen -W "{{choicesInvocation}}" -- "$cur") )""";
203175

204176
/// <summary>
205177
/// Generates a concrete set of bash completion selection for a given option.
@@ -208,7 +180,7 @@ private static string GenerateDynamicCall()
208180
/// </summary>
209181
/// <param name="option"></param>
210182
/// <returns>a bash switch case expression for providing completions for this option</returns>
211-
private static string? GenerateOptionHandler(CliOption option)
183+
internal static string? GenerateOptionHandler(CliOption option)
212184
{
213185
// unlike the completion-options generation, for actually implementing suggestions we should be able to handle all of the options' aliases.
214186
// this ensures if the user manually enters an alias we can support that usage.
@@ -250,7 +222,6 @@ private static string GenerateDynamicCall()
250222
}
251223
}
252224

253-
254225
public static class HelpExtensions
255226
{
256227
/// <summary>
@@ -313,4 +284,39 @@ public static string[] Names(this CliCommand command)
313284
}
314285
}
315286

287+
public static IEnumerable<CliOption> HeirarchicalOptions(this CliCommand c)
288+
{
289+
var myOptions = c.Options.Where(o => !o.Hidden);
290+
if (c.Parents.Count() == 0)
291+
{
292+
return myOptions;
293+
}
294+
else
295+
{
296+
return c.Parents.OfType<CliCommand>().SelectMany(OptionsForParent).Concat(myOptions);
297+
}
298+
299+
}
300+
301+
private static IEnumerable<CliOption> OptionsForParent(CliCommand c)
302+
{
303+
foreach (var o in c.Options)
304+
{
305+
if (o.Recursive)
306+
{
307+
if (!o.Hidden)
308+
{
309+
yield return o;
310+
}
311+
}
312+
}
313+
foreach (var p in c.Parents.OfType<CliCommand>())
314+
{
315+
foreach (var o in OptionsForParent(p))
316+
{
317+
yield return o;
318+
}
319+
}
320+
}
321+
316322
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.DotNet.Cli.Complete.Tests;
5+
6+
using Microsoft.DotNet.Cli.Completions.Shells;
7+
using System.CommandLine;
8+
using System.CommandLine.Help;
9+
10+
public class HelpExtensionsTests
11+
{
12+
[Fact]
13+
public void HelpOptionOnlyShowsUsefulNames()
14+
{
15+
new HelpOption().Names().Should().BeEquivalentTo(["--help", "-h"]);
16+
}
17+
18+
[Fact]
19+
public void OptionNamesListNameThenAliases()
20+
{
21+
new CliOption<string>("--name", "-n", "--nombre").Names().Should().Equal(["--name", "-n", "--nombre"]);
22+
}
23+
24+
[Fact]
25+
public void OptionsWithNoAliasesHaveOnlyOneName()
26+
{
27+
new CliOption<string>("--name").Names().Should().Equal(["--name"]);
28+
}
29+
30+
[Fact]
31+
public void HeirarchicalOptionsAreFlattened()
32+
{
33+
var parentCommand = new CliCommand("parent");
34+
var childCommand = new CliCommand("child");
35+
parentCommand.Subcommands.Add(childCommand);
36+
parentCommand.Options.Add(new CliOption<string>("--parent-global") { Recursive = true });
37+
parentCommand.Options.Add(new CliOption<string>("--parent-local") { Recursive = false });
38+
parentCommand.Options.Add(new CliOption<string>("--parent-global-but-hidden") { Recursive = true, Hidden = true });
39+
40+
childCommand.Options.Add(new CliOption<string>("--child-local"));
41+
childCommand.Options.Add(new CliOption<string>("--child-hidden") { Hidden = true });
42+
43+
// note: no parent-local or parent-global-but-hidden options, and no locally hidden options
44+
childCommand.HeirarchicalOptions().Select(c => c.Name).Should().Equal(["--parent-global", "--child-local"]);
45+
}
46+
}

test/dotnet.Tests/dotnet.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<Compile Include="..\dotnet-back-compat.Tests\**\*.cs" LinkBase="dotnet-back-compat" />
2929
<Compile Include="..\dotnet-build.Tests\**\*.cs" LinkBase="dotnet-build" />
3030
<Compile Include="..\dotnet-clean.Tests\**\*.cs" LinkBase="dotnet-clean" />
31+
<Compile Include="..\dotnet-complete.Tests\**\*.cs" LinkBase="dotnet-complete" />
3132
<Compile Include="..\dotnet-sdk-check.Tests\**\*.cs" LinkBase="dotnet-sdk-check" />
3233
<Compile Include="..\dotnet-format.Tests\*.cs" LinkBase="dotnet-format" />
3334
<Compile Include="..\dotnet-fsi.Tests\**\*.cs" LinkBase="dotnet-fsi" />

0 commit comments

Comments
 (0)