Skip to content

Commit

Permalink
Add tests for shell completion helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
baronfel committed Aug 23, 2024
1 parent 9944fcf commit b93f1b8
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 35 deletions.
76 changes: 41 additions & 35 deletions src/Cli/dotnet/commands/dotnet-completions/shells/Bash.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ private string GenerateCommandsCompletions(string[] parentCommandNames, CliComma
// generate the words for options and subcommands
var visibleSubcommands = command.Subcommands.Where(c => !c.Hidden).ToArray();
// notably, do not generate completions for all option aliases - since a user is tab-completing we can use the longest forms
var completionOptions = AllOptionsForCommand(command).Where(o => !o.Hidden).Select(o => o.Name).ToArray();
var completionOptions = command.HeirarchicalOptions().Where(o => !o.Hidden).Select(o => o.Name).ToArray();
var completionSubcommands = visibleSubcommands.Select(x => x.Name).ToArray();
string[] completionWords = [.. completionSubcommands, .. completionOptions];

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

private static IEnumerable<CliOption> AllOptionsForCommand(CliCommand c)
{
if (c.Parents.Count() == 0)
{
return c.Options;
}
else
{
return c.Options.Concat(c.Parents.OfType<CliCommand>().SelectMany(OptionsForParent));
}

}

private static IEnumerable<CliOption> OptionsForParent(CliCommand c)
{
foreach (var o in c.Options)
{
if (o.Recursive) yield return o;
}
foreach (var p in c.Parents.OfType<CliCommand>())
{
foreach (var o in OptionsForParent(p))
{
yield return o;
}
}
}

private static string[] PositionalArgumentTerms(CliArgument[] arguments)
internal static string[] PositionalArgumentTerms(CliArgument[] arguments)
{
var completions = new List<string>();
foreach (var argument in arguments)
Expand All @@ -176,12 +148,12 @@ private static string[] PositionalArgumentTerms(CliArgument[] arguments)
/// Generates a call to `dotnet complete <string> --position <int>` for dynamic completions where necessary, but in a more generic way
/// </summary>
/// <returns></returns>
private static string GenerateDynamicCall()
internal static string GenerateDynamicCall()
{
return $$"""${COMP_WORDS[0]} complete --position ${COMP_POINT} ${COMP_LINE} 2>/dev/null | tr '\n' ' '""";
}

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

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


public static class HelpExtensions
{
/// <summary>
Expand Down Expand Up @@ -313,4 +284,39 @@ public static string[] Names(this CliCommand command)
}
}

public static IEnumerable<CliOption> HeirarchicalOptions(this CliCommand c)
{
var myOptions = c.Options.Where(o => !o.Hidden);
if (c.Parents.Count() == 0)
{
return myOptions;
}
else
{
return c.Parents.OfType<CliCommand>().SelectMany(OptionsForParent).Concat(myOptions);
}

}

private static IEnumerable<CliOption> OptionsForParent(CliCommand c)
{
foreach (var o in c.Options)
{
if (o.Recursive)
{
if (!o.Hidden)
{
yield return o;
}
}
}
foreach (var p in c.Parents.OfType<CliCommand>())
{
foreach (var o in OptionsForParent(p))
{
yield return o;
}
}
}

}
46 changes: 46 additions & 0 deletions test/dotnet-complete.Tests/HelpExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.DotNet.Cli.Complete.Tests;

using Microsoft.DotNet.Cli.Completions.Shells;
using System.CommandLine;
using System.CommandLine.Help;

public class HelpExtensionsTests
{
[Fact]
public void HelpOptionOnlyShowsUsefulNames()
{
new HelpOption().Names().Should().BeEquivalentTo(["--help", "-h"]);
}

[Fact]
public void OptionNamesListNameThenAliases()
{
new CliOption<string>("--name", "-n", "--nombre").Names().Should().Equal(["--name", "-n", "--nombre"]);
}

[Fact]
public void OptionsWithNoAliasesHaveOnlyOneName()
{
new CliOption<string>("--name").Names().Should().Equal(["--name"]);
}

[Fact]
public void HeirarchicalOptionsAreFlattened()
{
var parentCommand = new CliCommand("parent");
var childCommand = new CliCommand("child");
parentCommand.Subcommands.Add(childCommand);
parentCommand.Options.Add(new CliOption<string>("--parent-global") { Recursive = true });
parentCommand.Options.Add(new CliOption<string>("--parent-local") { Recursive = false });
parentCommand.Options.Add(new CliOption<string>("--parent-global-but-hidden") { Recursive = true, Hidden = true });

childCommand.Options.Add(new CliOption<string>("--child-local"));
childCommand.Options.Add(new CliOption<string>("--child-hidden") { Hidden = true });

// note: no parent-local or parent-global-but-hidden options, and no locally hidden options
childCommand.HeirarchicalOptions().Select(c => c.Name).Should().Equal(["--parent-global", "--child-local"]);
}
}
1 change: 1 addition & 0 deletions test/dotnet.Tests/dotnet.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<Compile Include="..\dotnet-back-compat.Tests\**\*.cs" LinkBase="dotnet-back-compat" />
<Compile Include="..\dotnet-build.Tests\**\*.cs" LinkBase="dotnet-build" />
<Compile Include="..\dotnet-clean.Tests\**\*.cs" LinkBase="dotnet-clean" />
<Compile Include="..\dotnet-complete.Tests\**\*.cs" LinkBase="dotnet-complete" />
<Compile Include="..\dotnet-sdk-check.Tests\**\*.cs" LinkBase="dotnet-sdk-check" />
<Compile Include="..\dotnet-format.Tests\*.cs" LinkBase="dotnet-format" />
<Compile Include="..\dotnet-fsi.Tests\**\*.cs" LinkBase="dotnet-fsi" />
Expand Down

0 comments on commit b93f1b8

Please sign in to comment.