Skip to content

Commit 86ceeb6

Browse files
authored
Add suggestions when mistyping commands/options (#103)
* Add Damerau-Levenshtein suggestion provider * Implement Damerau-Levenshtein. * Remove "app.exe" in all tests * Refactor so help options are found by argument manager * Add suggestions to the output printer * Add test for null check * Add IConsole * Add suggestions to ouput * Add test to verify suggestions are not added to the output when no suggestions are available * Use Environment.NewLine
1 parent a2f6bc2 commit 86ceeb6

23 files changed

Lines changed: 770 additions & 94 deletions

CommandLineParser.Tests/BasicDITests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public void CommandLineParserUsesInjectedServiceCorrectly()
2727

2828
parser.RegisterCommand<MyCommandThatUsesService>();
2929

30-
var result = parser.Parse(new[] { "app.exe", "cmd" });
30+
var result = parser.Parse(new[] { "cmd" });
3131

3232
result.AssertNoErrors();
3333

CommandLineParser.Tests/Command/CommandDiscoveryTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public async Task CommandDiscoveryWithInjectedServices()
6868

6969
parser.DiscoverCommands(Assembly.GetExecutingAssembly());
7070

71-
var result = await parser.ParseAsync(new[] { "app.exe", "cmd" });
71+
var result = await parser.ParseAsync(new[] { "cmd" });
7272

7373
myServiceMock.Verify(_ => _.Call(), Times.Once());
7474
}
@@ -87,7 +87,7 @@ public async Task NonGenericCommandCanBeDiscovered()
8787

8888
parser.DiscoverCommands(typeof(NonGenericDiscoverableCommand).Assembly);
8989

90-
var result = await parser.ParseAsync(new[] { "app.exe", "cmd" });
90+
var result = await parser.ParseAsync(new[] { "cmd" });
9191

9292
Assert.True(parser.Commands.Count == 1);
9393

CommandLineParser.Tests/CommandLineParserTests.cs

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public void OrderAttributeWorks()
3838

3939
var parser = new CommandLineParser<OrderModel>(Services);
4040

41-
var result = parser.Parse(new string[] { "app.exe", from, to });
41+
var result = parser.Parse(new string[] { from, to });
4242

4343
result.AssertNoErrors();
4444

@@ -54,7 +54,7 @@ public void OrderAttributeWorks2()
5454

5555
var parser = new CommandLineParser<OrderModel>(Services);
5656

57-
var result = parser.Parse(new string[] { "app.exe", "-r", "5", from, to });
57+
var result = parser.Parse(new string[] { "-r", "5", from, to });
5858

5959
result.AssertNoErrors();
6060

@@ -76,7 +76,7 @@ public void OrderAttributeInCommandWorks()
7676
Assert.Equal(to, model.To);
7777
});
7878

79-
var result = parser.Parse(new string[] { "app.exe", "cmd", from, to });
79+
var result = parser.Parse(new string[] { "cmd", from, to });
8080

8181
result.AssertNoErrors();
8282
}
@@ -89,7 +89,7 @@ public void OrderedOptions_With_Named_Option_Between_Does_Not_work()
8989

9090
var parser = new CommandLineParser<OrderModel>(Services);
9191

92-
var result = parser.Parse(new string[] { "app.exe", from, "-r", "5", to });
92+
var result = parser.Parse(new string[] { from, "-r", "5", to });
9393

9494
Assert.True(result.HasErrors);
9595

@@ -105,7 +105,7 @@ public void OrderedOptions_With_Named_Option_Between_Does_Not_work2()
105105

106106
var parser = new CommandLineParser<OrderModelInt>(Services);
107107

108-
var result = parser.Parse(new string[] { "app.exe", from.ToString(), "oops", to.ToString() });
108+
var result = parser.Parse(new string[] { from.ToString(), "oops", to.ToString() });
109109

110110
Assert.True(result.HasErrors);
111111

@@ -123,7 +123,7 @@ public void StopProcessingWorks()
123123

124124
var parser = new CommandLineParser<OrderModelInt>(options, Services);
125125

126-
var result = parser.Parse(new string[] { "app.exe", "10", "20", "--", "some random stuff", "nothing to see here", "yadi yadi yadi", "-r", "10" });
126+
var result = parser.Parse(new string[] { "10", "20", "--", "some random stuff", "nothing to see here", "yadi yadi yadi", "-r", "10" });
127127

128128
result.AssertNoErrors();
129129

@@ -203,7 +203,7 @@ public void CommandLineParserUsesContainerCorrectly(bool generic)
203203
parser.RegisterCommand(typeof(MyCommand), typeof(object));
204204
}
205205

206-
var result = parser.Parse(new[] { "app.exe", "my" });
206+
var result = parser.Parse(new[] { "my" });
207207

208208
result.AssertNoErrors();
209209

@@ -238,7 +238,7 @@ public async Task CommandLineParserUsesContainerCorrectlyAsync(bool generic)
238238
parser.RegisterCommand(typeof(MyCommand), typeof(object));
239239
}
240240

241-
var result = await parser.ParseAsync(new[] { "app.exe", "my" });
241+
var result = await parser.ParseAsync(new[] { "my" });
242242

243243
result.AssertNoErrors();
244244

@@ -299,7 +299,7 @@ public void ParseTests()
299299
.Default("Default message")
300300
.Required();
301301

302-
var parsed = parser.Parse(new string[] { "app.exe", "-o", "test" });
302+
var parsed = parser.Parse(new string[] { "-o", "test" });
303303

304304
Assert.NotNull(parsed);
305305

@@ -309,11 +309,11 @@ public void ParseTests()
309309
}
310310

311311
[Theory]
312-
[InlineData(new[] { "app.exe", "-e", "Opt1" }, false, EnumOption.Opt1)]
313-
[InlineData(new[] { "app.exe", "-e=opt1" }, false, EnumOption.Opt1)]
314-
[InlineData(new[] { "app.exe", "-e", "Opt2" }, false, EnumOption.Opt2)]
315-
[InlineData(new[] { "app.exe", "-e", "bla" }, true, default(EnumOption))]
316-
[InlineData(new[] { "app.exe", "-e" }, true, default(EnumOption))]
312+
[InlineData(new[] { "-e", "Opt1" }, false, EnumOption.Opt1)]
313+
[InlineData(new[] { "-e=opt1" }, false, EnumOption.Opt1)]
314+
[InlineData(new[] { "-e", "Opt2" }, false, EnumOption.Opt2)]
315+
[InlineData(new[] { "-e", "bla" }, true, default(EnumOption))]
316+
[InlineData(new[] { "-e" }, true, default(EnumOption))]
317317
public void ParseEnumInArguments(string[] args, bool hasErrors, EnumOption enumOption)
318318
{
319319
var parser = new CommandLineParser<EnumOptions>(Services);
@@ -439,7 +439,7 @@ public async Task ParseWithCommandTestsAsync()
439439
.Name("m", "message")
440440
.Required();
441441

442-
var parsed = await parser.ParseAsync(new string[] { "app.exe", "-o", "test", "add", "-m=my message" });
442+
var parsed = await parser.ParseAsync(new string[] { "-o", "test", "add", "-m=my message" });
443443

444444
parsed.AssertNoErrors();
445445

@@ -451,8 +451,8 @@ public async Task ParseWithCommandTestsAsync()
451451
}
452452

453453
[Theory]
454-
[InlineData(new[] { "app.exe", "add", "-m", "message2", "-m", "message1" }, "message1", "message2")]
455-
[InlineData(new[] { "app.exe", "-m", "message1", "add", "-m", "message2" }, "message1", "message2")]
454+
[InlineData(new[] { "add", "-m", "message2", "-m", "message1" }, "message1", "message2")]
455+
[InlineData(new[] { "-m", "message1", "add", "-m", "message2" }, "message1", "message2")]
456456
[InlineData(new[] { "add", "-m", "message2", "-m", "message1" }, "message1", "message2")]
457457
[InlineData(new[] { "-m", "message1", "add", "-m", "message2" }, "message1", "message2")]
458458
public void ParseCommandTests(string[] args, string result1, string result2)
@@ -487,8 +487,8 @@ public void ParseCommandTests(string[] args, string result1, string result2)
487487
}
488488

489489
[Theory]
490-
[InlineData(new[] { "app.exe", "add", "-m", "message2", "-m", "message1" }, "message1", "message2")]
491-
[InlineData(new[] { "app.exe", "-m", "message1", "add", "-m", "message2" }, "message1", "message2")]
490+
[InlineData(new[] { "add", "-m", "message2", "-m", "message1" }, "message1", "message2")]
491+
[InlineData(new[] { "-m", "message1", "add", "-m", "message2" }, "message1", "message2")]
492492
[InlineData(new[] { "add", "-m", "message2", "-m", "message1" }, "message1", "message2")]
493493
[InlineData(new[] { "-m", "message1", "add", "-m", "message2" }, "message1", "message2")]
494494
public async Task ParseCommandTestsAsync(string[] args, string result1, string result2)
@@ -603,7 +603,7 @@ public void ConfigureTests()
603603
}
604604

605605
[Theory]
606-
[InlineData(new string[] { "" }, "defaulttransformed", false)]
606+
[InlineData(new string[] { }, "defaulttransformed", false)]
607607
[InlineData(new string[] { "-m", "test" }, "testtransformed", false)]
608608
[InlineData(new string[] { "--message", "test" }, "testtransformed", false)]
609609
public void TransformationWorksAsExpected(string[] args, string expected, bool errors)

CommandLineParser.Tests/Parsing/OptionClusteringTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public void ClusterdOptionsAreParsedCorrectly()
1515
{
1616
var parser = new CommandLineParser<ClusteredOptions<bool>>(Services);
1717

18-
var result = parser.Parse(new[] { "app.exe", "-abc" });
18+
var result = parser.Parse(new[] { "-abc" });
1919

2020
result.AssertNoErrors();
2121

@@ -31,7 +31,7 @@ public void ClusterdOptionsAllSetTheSameValue()
3131
{
3232
var parser = new CommandLineParser<ClusteredOptions<bool>>(Services);
3333

34-
var result = parser.Parse(new[] { "app.exe", "-abc", "false" });
34+
var result = parser.Parse(new[] { "-abc", "false" });
3535

3636
result.AssertNoErrors();
3737

@@ -47,7 +47,7 @@ public void ClusterdOptionsAreIgnoredWhenRepeated()
4747
{
4848
var parser = new CommandLineParser<ClusteredOptions<bool>>(Services);
4949

50-
var result = parser.Parse(new[] { "app.exe", "-abc", "false", "-abc", "true" });
50+
var result = parser.Parse(new[] { "-abc", "false", "-abc", "true" });
5151

5252
result.AssertNoErrors();
5353

@@ -70,7 +70,7 @@ public void ClusterdOptionsInCommandWork()
7070
Assert.False(model.C);
7171
});
7272

73-
var result = parser.Parse(new[] { "app.exe", "cmd", "-abc", "false" });
73+
var result = parser.Parse(new[] { "cmd", "-abc", "false" });
7474

7575
result.AssertNoErrors();
7676
}
@@ -87,7 +87,7 @@ public void ClusterdOptionsInCommandAndReusedInParentWork()
8787
Assert.False(model.C);
8888
});
8989

90-
var result = parser.Parse(new[] { "app.exe", "-abc", "cmd", "-abc", "false" });
90+
var result = parser.Parse(new[] { "-abc", "cmd", "-abc", "false" });
9191

9292
result.AssertNoErrors();
9393

@@ -108,7 +108,7 @@ public void ClusterdOptionsInCommandAndReusedInParentWork_String_Version()
108108
Assert.Equal("false", model.C);
109109
});
110110

111-
var result = parser.Parse(new[] { "app.exe", "-abc", "works", "cmd", "-abc", "false" });
111+
var result = parser.Parse(new[] { "-abc", "works", "cmd", "-abc", "false" });
112112

113113
result.AssertNoErrors();
114114

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
using MatthiWare.CommandLine.Abstractions;
2+
using MatthiWare.CommandLine.Abstractions.Command;
3+
using MatthiWare.CommandLine.Core.Usage;
4+
using Moq;
5+
using System.Collections.Generic;
6+
using Xunit;
7+
using Xunit.Abstractions;
8+
9+
namespace MatthiWare.CommandLine.Tests.Usage
10+
{
11+
public class DamerauLevenshteinTests : TestBase
12+
{
13+
public DamerauLevenshteinTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper)
14+
{
15+
}
16+
17+
[Theory]
18+
[InlineData(3, "CA", "ABC")]
19+
[InlineData(1, "Test", "Tst")]
20+
[InlineData(0, "Test", "Test")]
21+
[InlineData(4, "", "Test")]
22+
[InlineData(4, "Test", "")]
23+
public void DistanceOfStrings(int expected, string a, string b)
24+
{
25+
var dl = new DamerauLevenshteinSuggestionProvider();
26+
27+
Assert.Equal(expected, dl.FindDistance(a, b));
28+
}
29+
30+
[Fact]
31+
public void SuggestionsAreGatheredFromAllAvailablePlaces()
32+
{
33+
var containerMock = new Mock<ICommandLineCommandContainer>();
34+
var cmdMock = new Mock<ICommandLineCommand>();
35+
var optionMock = new Mock<ICommandLineOption>();
36+
37+
cmdMock.SetupGet(_ => _.Name).Returns("test1");
38+
optionMock.SetupGet(_ => _.HasLongName).Returns(true);
39+
optionMock.SetupGet(_ => _.HasShortName).Returns(true);
40+
optionMock.SetupGet(_ => _.LongName).Returns("test2");
41+
optionMock.SetupGet(_ => _.ShortName).Returns("test3");
42+
43+
containerMock.SetupGet(_ => _.Commands).Returns(AsList(cmdMock.Object));
44+
containerMock.SetupGet(_ => _.Options).Returns(AsList(optionMock.Object));
45+
46+
var dl = new DamerauLevenshteinSuggestionProvider();
47+
48+
var result = dl.GetSuggestions("test", containerMock.Object);
49+
50+
Assert.Contains("test1", result);
51+
Assert.Contains("test2", result);
52+
Assert.Contains("test3", result);
53+
54+
List<T> AsList<T>(T obj)
55+
{
56+
var list = new List<T>(1) { obj };
57+
return list;
58+
}
59+
}
60+
61+
[Fact]
62+
public void NoContainerReturnsEmptyResult()
63+
{
64+
var dl = new DamerauLevenshteinSuggestionProvider();
65+
66+
Assert.Empty(dl.GetSuggestions(string.Empty, null));
67+
}
68+
69+
[Fact]
70+
public void InvalidSuggestionIsNotReturned()
71+
{
72+
var containerMock = new Mock<ICommandLineCommandContainer>();
73+
var cmdMock = new Mock<ICommandLineCommand>();
74+
var optionMock = new Mock<ICommandLineOption>();
75+
76+
cmdMock.SetupGet(_ => _.Name).Returns("test");
77+
78+
containerMock.SetupGet(_ => _.Commands).Returns(AsList(cmdMock.Object));
79+
containerMock.SetupGet(_ => _.Options).Returns(new List<ICommandLineOption>());
80+
81+
var dl = new DamerauLevenshteinSuggestionProvider();
82+
83+
var result = dl.GetSuggestions("abc", containerMock.Object);
84+
85+
Assert.Empty(result);
86+
87+
List<T> AsList<T>(T obj)
88+
{
89+
var list = new List<T>(1) { obj };
90+
return list;
91+
}
92+
}
93+
94+
[Fact]
95+
public void NoSuggestionsReturnsEmpty()
96+
{
97+
var containerMock = new Mock<ICommandLineCommandContainer>();
98+
99+
containerMock.SetupGet(_ => _.Commands).Returns(new List<ICommandLineCommand>());
100+
containerMock.SetupGet(_ => _.Options).Returns(new List<ICommandLineOption>());
101+
102+
var dl = new DamerauLevenshteinSuggestionProvider();
103+
104+
var result = dl.GetSuggestions("abc", containerMock.Object);
105+
106+
Assert.Empty(result);
107+
}
108+
109+
[Fact]
110+
public void Test()
111+
{
112+
var parser = new CommandLineParser(Services);
113+
114+
parser.AddCommand().Name("cmd");
115+
116+
var result = parser.Parse(new[] { "cmdd" });
117+
118+
result.AssertNoErrors();
119+
}
120+
}
121+
}

CommandLineParser.Tests/Usage/NoColorOutputTests.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,18 @@ public NoColorOutputTests(ITestOutputHelper output) : base(output)
2323
var envMock = new Mock<IEnvironmentVariablesService>();
2424
envMock.SetupGet(env => env.NoColorRequested).Returns(() => variableServiceResult);
2525

26+
var consoleMock = new Mock<IConsole>();
27+
2628
variablesService = envMock.Object;
2729

2830
var usageBuilderMock = new Mock<IUsageBuilder>();
2931
usageBuilderMock.Setup(m => m.AddErrors(It.IsAny<IReadOnlyCollection<Exception>>())).Callback(() =>
3032
{
31-
consoleColorGetter(((UsagePrinter)parser.Printer).m_currentConsoleColor);
33+
consoleColorGetter(consoleMock.Object.ForegroundColor);
3234
});
3335

3436
Services.AddSingleton(envMock.Object);
37+
Services.AddSingleton(consoleMock.Object);
3538
Services.AddSingleton(usageBuilderMock.Object);
3639

3740
parser = new CommandLineParser<Options>(Services);

0 commit comments

Comments
 (0)