Files
Libation/Source/LibationCli/Program.cs
T
Cursor Agentandrmcrackan e0c780c998 Make --download-pending the CLI flag, with --not-downloaded as a legacy alias
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>
2026-08-25 02:42:56 +00:00

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;
}
}