-
Notifications
You must be signed in to change notification settings - Fork 421
Expand file tree
/
Copy pathCommand.cs
More file actions
258 lines (223 loc) · 11 KB
/
Command.cs
File metadata and controls
258 lines (223 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.Collections;
using System.Collections.Generic;
using System.CommandLine.Completions;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;
using System.Linq;
namespace System.CommandLine
{
/// <summary>
/// Represents a specific action that the application performs.
/// </summary>
/// <remarks>
/// Use the Command object for actions that correspond to a specific string (the command name). See
/// <see cref="RootCommand"/> for simple applications that only have one action. For example, <c>dotnet run</c>
/// uses <c>run</c> as the command.
/// </remarks>
public class Command : IdentifierSymbol, IEnumerable<Symbol>
{
private List<Argument>? _arguments;
private List<Option>? _options;
private List<Command>? _subcommands;
private List<ValidateSymbolResult<CommandResult>>? _validators;
/// <summary>
/// Initializes a new instance of the Command class.
/// </summary>
/// <param name="name">The name of the command.</param>
/// <param name="description">The description of the command, shown in help.</param>
public Command(string name, string? description = null) : base(name, description)
{
}
/// <summary>
/// Gets the child symbols.
/// </summary>
public IEnumerable<Symbol> Children
{
get
{
foreach (var command in Subcommands)
yield return command;
foreach (var option in Options)
yield return option;
foreach (var argument in Arguments)
yield return argument;
}
}
/// <summary>
/// Represents all of the arguments for the command.
/// </summary>
public IReadOnlyList<Argument> Arguments => _arguments is not null ? _arguments : Array.Empty<Argument>();
internal bool HasArguments => _arguments is not null;
/// <summary>
/// Represents all of the options for the command, including global options that have been applied to any of the command's ancestors.
/// </summary>
public IReadOnlyList<Option> Options => _options is not null ? _options : Array.Empty<Option>();
/// <summary>
/// Represents all of the subcommands for the command.
/// </summary>
public IReadOnlyList<Command> Subcommands => _subcommands is not null ? _subcommands : Array.Empty<Command>();
internal IReadOnlyList<ValidateSymbolResult<CommandResult>> Validators
=> _validators is not null ? _validators : Array.Empty<ValidateSymbolResult<CommandResult>>();
internal bool HasValidators => _validators is not null; // initialized by Add method, so when it's not null the Count is always > 0
/// <summary>
/// Adds an <see cref="Argument"/> to the command.
/// </summary>
/// <param name="argument">The argument to add to the command.</param>
public void AddArgument(Argument argument)
{
argument.AddParent(this);
(_arguments ??= new()).Add(argument);
}
/// <summary>
/// Adds a subcommand to the command.
/// </summary>
/// <param name="command">The subcommand to add to the command.</param>
/// <remarks>Commands can be nested to an arbitrary depth.</remarks>
public void AddCommand(Command command)
{
command.AddParent(this);
(_subcommands ??= new()).Add(command);
}
/// <summary>
/// Adds an <see cref="Option"/> to the command.
/// </summary>
/// <param name="option">The option to add to the command.</param>
public void AddOption(Option option)
{
option.AddParent(this);
(_options ??= new()).Add(option);
}
/// <summary>
/// Adds a global <see cref="Option"/> to the command.
/// </summary>
/// <param name="option">The global option to add to the command.</param>
/// <remarks>Global options are applied to the command and recursively to subcommands. They do not apply to
/// parent commands.</remarks>
public void AddGlobalOption(Option option)
{
option.IsGlobal = true;
AddOption(option);
}
/// <summary>
/// Adds an <see cref="Option"/> to the command.
/// </summary>
/// <param name="option">The option to add to the command.</param>
public void Add(Option option) => AddOption(option);
/// <summary>
/// Adds an <see cref="Argument"/> to the command.
/// </summary>
/// <param name="argument">The argument to add to the command.</param>
public void Add(Argument argument) => AddArgument(argument);
/// <summary>
/// Adds a subcommand to the command.
/// </summary>
/// <param name="command">The subcommand to add to the command.</param>
/// <remarks>Commands can be nested to an arbitrary depth.</remarks>
public void Add(Command command) => AddCommand(command);
private protected override string DefaultName => throw new NotImplementedException();
/// <summary>
/// Adds a custom validator to the command. Validators can be used
/// to create custom validation logic.
/// </summary>
/// <param name="validate">The delegate to validate the symbols during parsing.</param>
public void AddValidator(ValidateSymbolResult<CommandResult> validate) => (_validators ??= new()).Add(validate);
/// <summary>
/// Gets or sets a value that indicates whether unmatched tokens should be treated as errors. For example,
/// if set to <see langword="true"/> and an extra command or argument is provided, validation will fail.
/// </summary>
public bool TreatUnmatchedTokensAsErrors { get; set; } = true;
/// <summary>
/// Gets or sets the <see cref="ICommandHandler"/> for the command. The handler represents the action
/// that will be performed when the command is invoked.
/// </summary>
/// <remarks>
/// <para>Use one of the <see cref="Handler.SetHandler(Command, Action)" /> overloads to construct a handler.</para>
/// <para>If the handler is not specified, parser errors will be generated for command line input that
/// invokes this command.</para></remarks>
public ICommandHandler? Handler { get; set; }
/// <summary>
/// Represents all of the symbols for the command.
/// </summary>
public IEnumerator<Symbol> GetEnumerator() => Children.GetEnumerator();
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
internal Parser? ImplicitInvocationParser { get; set; }
internal Parser? ImplicitSimpleParser { get; set; }
/// <inheritdoc />
public override IEnumerable<CompletionItem> GetCompletions(CompletionContext context)
{
var completions = new List<CompletionItem>();
if (context.WordToComplete is { } textToMatch)
{
var commands = Subcommands;
for (int i = 0; i < commands.Count; i++)
{
AddCompletionFor(commands[i], textToMatch);
}
var options = Options;
for (int i = 0; i < options.Count; i++)
{
AddCompletionFor(options[i], textToMatch);
}
var arguments = Arguments;
for (int i = 0; i < arguments.Count; i++)
{
var argument = arguments[i];
foreach (var completion in argument.GetCompletions(context))
{
if (completion.Label.ContainsCaseInsensitive(textToMatch))
{
completions.Add(completion);
}
}
}
foreach (var parent in Parents.FlattenBreadthFirst(p => p.Parents))
{
if (parent is Command parentCommand)
{
for (var i = 0; i < parentCommand.Options.Count; i++)
{
var option = parentCommand.Options[i];
if (option.IsGlobal)
{
AddCompletionFor(option, textToMatch);
}
}
}
}
}
return completions
.OrderBy(item => item.SortText.IndexOfCaseInsensitive(context.WordToComplete))
.ThenBy(symbol => symbol.Label, StringComparer.OrdinalIgnoreCase);
// 'best' is a bit of a misnomer here. We want to return one and only one completion itme for each option,
// but depending on what has already been entered by the user the algorithm changes.
// For empty input, we return the longest alias of the set of aliases (this matches the DefaultName logic in Option.cs, but does not remove
// any prefixes (--, etc)).
// For nonempty input, we find all tokens that contain the input and then return the first one sorted by:
// * shortest Levenstein distance, then by
// * longest common startswith substring
string? FindBestCompletionFor(string textToMatch, IReadOnlyCollection<string> aliases) => textToMatch switch
{
#if NET6_0_OR_GREATER
"" => aliases.MaxBy(a => a.Length), // find the longest alias
(string stem) => aliases.Where(a => a.Contains(stem, StringComparison.OrdinalIgnoreCase)).OrderByDescending(a => TokenDistances.GetLevensteinDistance(stem, a)).ThenByDescending(a => TokenDistances.GetStartsWithDistance(stem, a)).FirstOrDefault()
#else
"" => aliases.OrderByDescending(a => a.Length).FirstOrDefault(), // find the longest alias
(string stem) => aliases.Where(a => a.Contains(stem)).OrderByDescending(a => TokenDistances.GetLevensteinDistance(stem, a)).ThenByDescending(a => TokenDistances.GetStartsWithDistance(stem, a)).FirstOrDefault()
#endif
};
void AddCompletionFor(IdentifierSymbol identifier, string textToMatch)
{
if (!identifier.IsHidden)
{
var bestAlias = FindBestCompletionFor(textToMatch, identifier.Aliases);
if (bestAlias is not null) {
completions.Add(new CompletionItem(bestAlias, CompletionItemKind.Keyword, detail: identifier.Description));
}
}
}
}
}
}