mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-09-12 21:57:19 -04:00
Empty-args usage now lists nested command groups alongside root verbs. Nested `abs upload --help` brands the public command path instead of the internal parser verb. Co-authored-by: rmcrackan <rmcrackan@gmail.com>
258 lines
7.8 KiB
C#
258 lines
7.8 KiB
C#
using CommandLine;
|
|
using CommandLine.Text;
|
|
using Dinah.Core;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace LibationCli;
|
|
|
|
file static class GlobalCliHelp
|
|
{
|
|
internal static bool IsGlobalHelpToken(string token)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(token))
|
|
return false;
|
|
return token.Equals("--help", StringComparison.OrdinalIgnoreCase)
|
|
|| token.Equals("-h", StringComparison.OrdinalIgnoreCase)
|
|
|| token.Equals("/?", StringComparison.OrdinalIgnoreCase)
|
|
|| token.Equals("/h", StringComparison.OrdinalIgnoreCase)
|
|
|| token.Equals("/help", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
}
|
|
|
|
public enum ExitCode
|
|
{
|
|
ProcessCompletedSuccessfully = 0,
|
|
NonRunNonError = 1,
|
|
ParseError = 2,
|
|
RunTimeError = 3
|
|
}
|
|
|
|
internal sealed record CliParseOutcome(ParserResult<object>? Result, ExitCode? ExitCode);
|
|
|
|
class Program
|
|
{
|
|
public static readonly Type[] VerbTypes = Setup.LoadVerbs()
|
|
.Where(type => type != typeof(AbsUploadOptions))
|
|
.ToArray();
|
|
static async Task Main(string[] args)
|
|
{
|
|
Console.OutputEncoding = Console.InputEncoding = System.Text.Encoding.UTF8;
|
|
#if DEBUG
|
|
string input = "";
|
|
|
|
//input = " set-status -n --force B017V4IM1G";
|
|
//input = " liberate B017V4IM1G";
|
|
//input = " convert B017V4IM1G";
|
|
//input = " search \"-liberated\"";
|
|
//input = " export --help";
|
|
//input = " version --check";
|
|
//input = " scan rmcrackan";
|
|
//input = " help set-status";
|
|
//input = " liberate ";
|
|
//input = "get-setting -o Replace_OpenQuote=[ ";
|
|
//input = "get-setting ";
|
|
//input = "liberate B017V4NOZ0 --force -o Books=\"./Books\"";
|
|
|
|
// note: this hack will fail for quoted file paths with spaces because it will break on those spaces
|
|
if (!string.IsNullOrWhiteSpace(input))
|
|
args = input.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
var setBreakPointHere = args;
|
|
#endif
|
|
var outcome = ParseInvocation(args, Console.Error);
|
|
if (outcome.ExitCode is { } exitCode)
|
|
{
|
|
Environment.ExitCode = (int)exitCode;
|
|
return;
|
|
}
|
|
|
|
//Everything parsed correctly, so execute the command
|
|
// async: run parsed options
|
|
await outcome.Result!.WithParsedAsync<OptionsBase>(opt => opt.Run());
|
|
}
|
|
|
|
internal static CliParseOutcome ParseInvocation(
|
|
string[] args,
|
|
TextWriter error,
|
|
IReadOnlyList<CliCommandGroup>? groups = null)
|
|
{
|
|
var route = CliCommandRouter.Route(args, groups ?? CliCommandGroups.All);
|
|
if (route.Kind == CliRouteKind.GroupHelp)
|
|
{
|
|
error.WriteLine(HelpVerb.GetGroupHelpText(route.Group!));
|
|
return new(null, ExitCode.ProcessCompletedSuccessfully);
|
|
}
|
|
|
|
args = route.ParserArgs;
|
|
if (route.Kind == CliRouteKind.UnknownSubcommand)
|
|
{
|
|
error.WriteLine($"Unknown {route.Group!.Name.ToUpperInvariant()} command '{args[1]}'.");
|
|
error.WriteLine(HelpVerb.GetGroupHelpText(route.Group!));
|
|
return new(null, ExitCode.ParseError);
|
|
}
|
|
|
|
if (TryPrintGlobalHelpOnly(args, error))
|
|
return new(null, ExitCode.ProcessCompletedSuccessfully);
|
|
|
|
args = NormalizeVerbShortHelpAliases(args);
|
|
|
|
Type[] parserVerbTypes = route.Subcommand is { } subcommand
|
|
? [subcommand.OptionsType]
|
|
: VerbTypes;
|
|
var result = new Parser(ConfigureParser).ParseArguments(args, parserVerbTypes);
|
|
|
|
if (result.Value is HelpVerb helper)
|
|
{
|
|
// AutoHelp normally intercepts bare `help` as HelpVerbRequestedError. Keep this path
|
|
// consistent if CommandLineParser ever hands us a parsed HelpVerb instead.
|
|
if (string.IsNullOrWhiteSpace(helper.HelpType))
|
|
WriteGlobalVerbListHelp(error);
|
|
else
|
|
error.WriteLine(helper.GetHelpText());
|
|
return new(result, ExitCode.ProcessCompletedSuccessfully);
|
|
}
|
|
|
|
if (result.TypeInfo.Current == typeof(HelpVerb))
|
|
{
|
|
//Error parsing the command, but the verb type was identified as HelpVerb
|
|
WriteGlobalVerbListHelp(error);
|
|
return new(result, ExitCode.ProcessCompletedSuccessfully);
|
|
}
|
|
|
|
if (result.Errors.Any())
|
|
return new(result, HandleErrors(result, error, route));
|
|
|
|
return new(result, null);
|
|
}
|
|
|
|
private static ExitCode HandleErrors(ParserResult<object> result, TextWriter error, CliRoute route)
|
|
{
|
|
var errorsList = result.Errors.ToList();
|
|
|
|
if (errorsList.Any(e => e.Tag == ErrorType.HelpRequestedError))
|
|
{
|
|
WriteVerbOptionsHelp(result, error, route);
|
|
return ExitCode.ProcessCompletedSuccessfully;
|
|
}
|
|
|
|
if (errorsList.OfType<HelpVerbRequestedError>().FirstOrDefault() is { } helpVerbErr)
|
|
{
|
|
WriteHelpForVerbRequestedError(helpVerbErr, error);
|
|
return ExitCode.ProcessCompletedSuccessfully;
|
|
}
|
|
|
|
if (errorsList.Any(e => e.Tag == ErrorType.VersionRequestedError))
|
|
{
|
|
error.WriteLine(HelpVerb.CreateHelpText().Heading);
|
|
return ExitCode.ProcessCompletedSuccessfully;
|
|
}
|
|
|
|
if (errorsList.OfType<NoVerbSelectedError>().Any())
|
|
{
|
|
WriteGlobalVerbListHelp(error, "No verb selected");
|
|
return ExitCode.ParseError;
|
|
}
|
|
|
|
//print the specified verb's usage
|
|
var helpText = HelpVerb.CreateHelpText();
|
|
helpText.AddDashesToOption = true;
|
|
helpText.AutoHelp = true;
|
|
|
|
if (!errorsList.OfType<UnknownOptionError>().Any(o => o.Token.ToLower() == "help"))
|
|
{
|
|
//verb was not executed with the "--help" option,
|
|
//so print verb option parsing error info.
|
|
helpText = HelpText.DefaultParsingErrorsHandler(result, helpText);
|
|
}
|
|
|
|
AddNestedCommandHeading(helpText, route);
|
|
helpText.AddOptions(result);
|
|
error.WriteLine(helpText);
|
|
return ExitCode.ParseError;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Multi-verb parsing treats the first token as a verb name, so bare <c>--help</c> / <c>-h</c> must be handled here.
|
|
/// </summary>
|
|
private static bool TryPrintGlobalHelpOnly(string[] args, TextWriter error)
|
|
{
|
|
if (args is not { Length: 1 } || !GlobalCliHelp.IsGlobalHelpToken(args[0]))
|
|
return false;
|
|
|
|
WriteGlobalVerbListHelp(error);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// CommandLineParser's implicit help is <c>--help</c> only; map the first <c>-h</c> after the verb (case-insensitive, so <c>-H</c> too) to <c>--help</c>.
|
|
/// </summary>
|
|
private static string[] NormalizeVerbShortHelpAliases(string[] args)
|
|
{
|
|
if (args.Length < 2)
|
|
return args;
|
|
|
|
var copy = (string[])args.Clone();
|
|
for (var i = 1; i < copy.Length; i++)
|
|
{
|
|
if (copy[i].Equals("-h", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
copy[i] = "--help";
|
|
break;
|
|
}
|
|
}
|
|
|
|
return copy;
|
|
}
|
|
|
|
private static void WriteGlobalVerbListHelp(TextWriter error, string? preOptionsLine = null)
|
|
=> HelpVerb.WriteGlobalVerbList(error, VerbTypes, CliCommandGroups.All, preOptionsLine);
|
|
|
|
private static void WriteVerbOptionsHelp(ParserResult<object> result, TextWriter error, CliRoute? route = null)
|
|
{
|
|
var helpText = HelpVerb.CreateHelpText();
|
|
AddNestedCommandHeading(helpText, route);
|
|
helpText.AddDashesToOption = true;
|
|
helpText.AutoHelp = true;
|
|
helpText.AddOptions(result);
|
|
error.WriteLine(helpText);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Nested commands are rewritten to an internal parser verb (e.g. <c>upload</c>).
|
|
/// Brand help with the public path (<c>abs upload</c>) so the usage screen matches what users type.
|
|
/// </summary>
|
|
private static void AddNestedCommandHeading(HelpText helpText, CliRoute? route)
|
|
{
|
|
if (route?.Group is not { } group || route.Subcommand is not { } subcommand)
|
|
return;
|
|
|
|
helpText.AddPreOptionsLine($"{group.Name} {subcommand.Name}");
|
|
helpText.AddPreOptionsLine(subcommand.HelpText);
|
|
}
|
|
|
|
private static void WriteHelpForVerbRequestedError(HelpVerbRequestedError helpVerbErr, TextWriter error)
|
|
{
|
|
if (!helpVerbErr.Matched || helpVerbErr.Type is null || string.IsNullOrWhiteSpace(helpVerbErr.Verb))
|
|
{
|
|
WriteGlobalVerbListHelp(error);
|
|
return;
|
|
}
|
|
|
|
var subResult = new Parser(ConfigureParser).ParseArguments(new[] { helpVerbErr.Verb }, VerbTypes);
|
|
if (subResult.TypeInfo.Current != typeof(NullInstance))
|
|
WriteVerbOptionsHelp(subResult, error);
|
|
else
|
|
WriteGlobalVerbListHelp(error);
|
|
}
|
|
|
|
private static void ConfigureParser(ParserSettings settings)
|
|
{
|
|
settings.AllowMultiInstance = true;
|
|
settings.AutoVersion = false;
|
|
settings.AutoHelp = true;
|
|
}
|
|
}
|