It should be possible to access all the command configuration
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 5/5
- Thời gian dự kiến
- Hơn một tuần
- Mức phù hợp với người mới
- 28/100
Hướng nghiên cứu
Bắt đầu từ các API Argument và Option liên quan đến AcceptOnlyFromAmong và lệnh gọi rootCommand.Parse được hiển thị trong Program.cs/Main. Theo dõi cách cấu hình completion được lưu trữ và cung cấp, sau đó xác định một API công khai cho phép các caller tương tác đọc metadata của command, argument, option và các giá trị được phép mà không dùng reflection. Hoàn tất khi use case của issue có thể truy cập cấu hình đó thông qua các API được hỗ trợ.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
I originally raised this as #2526, but that was on beta4 and the internals have changed since then.
I have a large command line application, with lots of sub commands and options, it's used both in scheduled tasks (where any parse fail should error) and by users (where it should be interactive).
I want to use --interactive as a global option - if parse succeeds this is ignored (it just runs) but if parse fails (and this is true) the CLI app should prompt the user for the missing command/argument/options.
The information is all there for building the help, but when --interactive is set I want to prompt for just the next bit, not stop and present the help.
Unfortunately, that information is all hidden in the private implementation. For instance, I can set AcceptOnlyFromAmong in my argument or option, but then when I prompt the user for that argument I can't access that list.
Here is an example use-case (using Spectre.Console for the prompt formatting, but you can use any console input):
static class InteractiveParsePrompt
{
/// <summary>Limit of error depth - users can CTRL+C cancel out but this is a saftey check.</summary>
const int MaxDepth = 20;
/// <summary>Interative option, set globally and available on all commands.</summary>
public static readonly Option<bool> OptInteractive = new("--interactive", "--i", "-i") {
Description = "Run in interactive mode, any missing arguments or options will be requested",
Recursive = true,
};
/// <summary>Escape/quote text so that it can be passed on the command line</summary>
static string EscapeCommandLine(string value)
{
bool allSafe = true;
foreach (char c in value)
if(!char.IsLetter(c) &&
!char.IsNumber(c) &&
c != '-')
{
allSafe = false;
break;
}
if (allSafe) return value;
return $"\"{value.Replace("\"", "\\\"")}\"";
}
/// <summary>Strip leading hyphens from the name, as it might include them but doesn't have to</summary>
static string NormaliseOptionName(string name)
{
if (name.StartsWith("--"))
return name[2..];
else if (name.StartsWith("-"))
return name[1..];
else
return name;
}
/// <summary>Get the allowed values for an argument.</summary>
static string[] GetAllowedValues(Argument argument) {
// System.CommandLine makes all this info about the allowed values private, so we need to use reflection to get it
var target = argument.CompletionSources?.FirstOrDefault()?.Target;
if (target is null) return [];
// We expect this to be Func<CompletionContext, IEnumerable<CompletionItem>>
// Why we need to decompile and reflect to get values set by AcceptOnlyFromAmong is baffling
// Obviously this is fragile AF so expect bugs
var method = target.GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)?.FirstOrDefault();
if (method is null) return [];
// Invoke the method to get the list of CompletionItem set by AcceptOnlyFromAmong
var allowedValues = method.Invoke(target, BindingFlags.NonPublic | BindingFlags.Instance, null, new CompletionContext[1], null) as IEnumerable<CompletionItem>;
if (allowedValues is null) return [];
return [..from v in allowedValues let t = v.InsertText where !string.IsNullOrEmpty(t) select t];
}
/// <summary>Get the markup text for the option or argument description.</summary>
static string PromptText(Symbol symbol) {
if (symbol.Description is not null)
return $"[bold yellow]{symbol.Name}[/] [italic]{symbol.Description.EscapeMarkup().TrimEnd(' ', '.')}[/]" ;
return $"[bold yellow]{symbol.Name}[/]";
}
/// <summary>Prompt the user to provide the value for an argument</summary>
static async Task<string> PromptArgument(
Argument argument,
IAnsiConsole console,
CancellationToken cancellationToken)
{
string[] allowedValues = GetAllowedValues(argument);
IPrompt<string> prompt;
if (allowedValues.Length > 0)
prompt = new SelectionPrompt<string>().
Title(PromptText(argument)).
PageSize(20).
AddChoices(allowedValues.Order());
else
prompt = new TextPrompt<string>(PromptText(argument));
string argResponse = await prompt.ShowAsync(console, cancellationToken);
console.MarkupLine($"Argument [bold yellow]{argument.Name}[/] = [green]{argResponse}[/]");
return argResponse;
}
/// <summary>Prompt the user to provide the value for an option</summary>
static async Task<string[]> PromptOption(
Option option,
IAnsiConsole console,
CancellationToken cancellationToken)
{
if (option.ValueType == typeof(bool)) {
// Boolean, give them a y/n confirmation prompt
var promptConfirm = new TextPrompt<bool>(PromptText(option)).
AddChoice(true).
AddChoice(false).
DefaultValue(false).
WithConverter(choice => choice ? "y" : "n");
bool optConfirm = await promptConfirm.ShowAsync(console, cancellationToken);
if (!optConfirm) return [];
console.MarkupLine($"Option set [bold green]{option.Name}[/]");
return [$"--{NormaliseOptionName(option.Name)}"];
}
TextPrompt<string> prompt = new(PromptText(option));
// Get the default value, if the option has one
// option.GetDefaultValue() throws an (undocumented) exception if there is no default value factory set
string? defaultValue = option.HasDefaultValue ? option.GetDefaultValue()?.ToString() : null;
if (defaultValue is not null)
prompt.DefaultValue(defaultValue);
string optResponse = await prompt.ShowAsync(console, cancellationToken);
console.MarkupLine($"Option [bold yellow]{option.Name}[/] = [green]{optResponse}[/]");
return [$"--{NormaliseOptionName(option.Name)}", optResponse];
}
/// <summary>Prompt the user to choose a subcommand, if that has arguments or options prompt for them too, return a new set of arguments to parse from the prompts</summary>
static async IAsyncEnumerable<string> PromptCommand(
Command command,
IAnsiConsole console,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
int maxL = command.Subcommands.Select(c => c.Name.Length).Max() + 1;
string subCommand = console.Prompt(
new SelectionPrompt<string>().
Title("Choose command?").
PageSize(20).
AddChoices(command.Subcommands.Select(c => $"{c.Name.PadRight(maxL)}: {c.Description}").Order()));
string commandName = subCommand.Split(":")[0].Trim();
console.MarkupLine($"Command [green]{commandName}[/] selected");
yield return commandName;
var subCommandFound = command.Subcommands.FirstOrDefault(c => c.Name == commandName);
if(subCommandFound is null) yield break;
if(subCommandFound.Arguments.Count > 0)
foreach (var argument in subCommandFound.Arguments)
yield return await PromptArgument(argument, console, cancellationToken);
if (subCommandFound.Options.Count > 0)
foreach (var option in subCommandFound.Options)
foreach(string optionValue in await PromptOption(option, console, cancellationToken))
yield return optionValue;
if (subCommandFound.Subcommands.Count > 0)
await foreach (string sub in PromptCommand(subCommandFound, console, cancellationToken))
yield return sub;
}
/// <summary>Intercept the command line parse on failure if --interactive option is set to prompt the user for the missing commands, arguments and options.</summary>
public static async Task<ParseResult> AskUserForArgs(IServiceProvider services, RootCommand rootCommand, ParseResult original, CancellationToken cancellationToken)
{
// If no errors or not in interactive mode, continue
if (!original.GetValue(OptInteractive) ||
original.Errors.Count == 0)
return original;
// Use Spectre.Console for interactive prompts, set up in the DI
var console = services.GetRequiredService<IAnsiConsole>();
console.WriteLine("Interactive mode");
var parseResult = original;
try {
int retry = 0;
while(retry++ < MaxDepth &&
parseResult.Errors.Count != 0 &&
!cancellationToken.IsCancellationRequested)
{
var command = parseResult.CommandResult.Command;
List<string> interactiveArgs = [.. parseResult.Tokens.Select(t => t.Value)];
foreach (var error in parseResult.Errors)
{
if (cancellationToken.IsCancellationRequested) break;
if (error.Message == "Required command was not provided.")
{
await foreach (string arg in PromptCommand(command, console, cancellationToken))
{
if (cancellationToken.IsCancellationRequested) break;
interactiveArgs.Add(arg);
}
}
else if (error.Message.StartsWith("Required argument missing for command:"))
{
string argumentName = error.Message.Split(":")[1].Trim(' ', '\'', '.');
var argument = command.Arguments.FirstOrDefault(a => a.Name == argumentName);
if (argument is null)
{
console.MarkupLine($"[red]{error.Message.EscapeMarkup()}[/]");
break;
}
interactiveArgs.Add(await PromptArgument(argument, console, cancellationToken));
}
else
{
console.MarkupLine($"[red]{error.Message.EscapeMarkup()}[/]");
break;
}
}
parseResult = rootCommand.Parse(interactiveArgs);
}
}
catch (TaskCanceledException) { } // Ignore cancellation errors
if (cancellationToken.IsCancellationRequested)
console.MarkupLine("[red]Cancelled[/]");
else if (parseResult.Errors.Count == 0)
{
string newArgs = string.Join(' ', parseResult.Tokens.Select(t => EscapeCommandLine(t.Value)));
console.MarkupLine($"New arguments set: [green]{newArgs.EscapeMarkup()}[/]");
}
else
console.MarkupLine("[red]Failed[/]");
return parseResult;
}
}
And then I cam hook this up in Program.cs/Main:
var parsed = rootCommand.Parse(args);
// If --interactive and there were parsing errors, prompt the user to fix them
if (parsed.GetValue(InteractiveParsePrompt.OptInteractive) &&
parsed.Errors.Count > 0)
parsed = await InteractiveParsePrompt.AskUserForArgs(services, rootCommand, parsed, cancellationToken);
Why do I need reflection to get the values from AcceptOnlyFromAmong that I set from my code?
- Ngôn ngữ chính
- C#
- Star
- 3.7k
- Fork
- 436
- Merge trung bình
- 6 giờ 27 phút
- Pull request đã merge (30 ngày)
- 2
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Không có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của dotnet/command-line-api
-
German localization is incompleteCó thể đã có người làm @b-v-d-e-v đã nhận 34 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
dotnet/command-line-api#2852 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Incomplete French (fr) translation: RequiredOptionWasNotProvided not translatedCó thể đã có người làm @JPBlanc đã nhận 132 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
dotnet/command-line-api#2822 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
dotnet/command-line-api#2792 · 2 bình luận · 17 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
-
`Argument.Arity` modify internal state on first get access.Có thể đã có người làm @zjdangge đã nhận 19 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
dotnet/command-line-api#2704 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
GetCompletions should check exit code of invoked applicationCó thể đã có người làm @baradgur đã nhận 1303 ngày trước. Đang mởArea-Completions bug help wanted
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
dotnet/command-line-api#2137 · 1 bình luận · 3 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của dotnet/command-line-api
Issue tương tự
-
The Recommended preset turns on Import Uno DSP, which only works with the Material themeCó thể đã có người làm @agneszitte đã nhận hôm nay. Đang mở
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 92/100
unoplatform/uno.templates#2388 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 77/100
eyalgolan/statecraft-israel#2732 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
変換の候補のどれが Zenzai の答えか分からないĐang mở要トリアージ
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 64/100
Maintainer thường phản hồi trong vòng 1 ngày
-
kind/chore status/draft
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
Altinn/altinn-events#1158 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
S: Untriaged
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
Trauma-Station/Trauma-Station#5285 ·
Maintainer thường phản hồi trong vòng 1 ngày