mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-09-13 06:07:30 -04:00
The status this flag sets is now called 'Download Pending', so -p / --download-pending is the name the help offers. -n / --not-downloaded keeps working. It is what years of scripts, forum answers and issue comments tell people to run, so breaking it would cost more than the inconsistency is worth. It is hidden from --help so the new name is the only one advertised, and both flags feed one SetPending property that the verb acts on. All three names stay in the 'Download Status' option group, so 'at least one status flag is required' still holds. That error message does enumerate the group, which is the one place the legacy name still surfaces. 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 -p --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;
|
|
}
|
|
}
|