From 486ee0367c736913796f758c52136aeee7672909 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Tue, 30 Jun 2026 19:46:37 +0200 Subject: [PATCH 01/70] Prepare for OpenCli --- .../Annotations/ExitCodeAttribute.cs | 16 ++ .../Annotations/SwitchAttribute.cs | 4 +- Source/BookGen.Cli/CommandRunner.cs | 70 +++++++- Source/BookGen.Cli/CommandRunnerProxy.cs | 16 +- Source/BookGen.Cli/CommandRunnerSettings.cs | 6 + Source/BookGen.Cli/ICommandRunnerProxy.cs | 7 +- Source/BookGen.Cli/IGlobalOptionParser.cs | 51 ++++++ Source/BookGen.Cli/OpenCli/Draft/Argument.cs | 66 ++++++++ Source/BookGen.Cli/OpenCli/Draft/Arity.cs | 48 ++++++ Source/BookGen.Cli/OpenCli/Draft/CliInfo.cs | 54 +++++++ Source/BookGen.Cli/OpenCli/Draft/Command.cs | 90 +++++++++++ Source/BookGen.Cli/OpenCli/Draft/Contact.cs | 36 +++++ .../BookGen.Cli/OpenCli/Draft/Conventions.cs | 28 ++++ Source/BookGen.Cli/OpenCli/Draft/Document.cs | 40 +++++ Source/BookGen.Cli/OpenCli/Draft/ExitCode.cs | 24 +++ Source/BookGen.Cli/OpenCli/Draft/License.cs | 35 ++++ Source/BookGen.Cli/OpenCli/Draft/Metadata.cs | 17 ++ Source/BookGen.Cli/OpenCli/Draft/Option.cs | 74 +++++++++ .../OpenCli/OpenCliDraftGenerator.cs | 152 ++++++++++++++++++ Source/BookGen.Cli/ProgramMetaData.cs | 24 +++ .../CommandCode/Cdg/CdgArguments.cs | 2 +- .../CommandCode/Organize/OrganizeArguments.cs | 2 +- Source/BookGen.Shellprog/Program.cs | 1 + Source/BookGen/BookGenArgumentBase.cs | 6 +- Source/BookGen/BuildArguments.cs | 4 +- Source/BookGen/Commands/AssemblyDocument.cs | 4 +- .../BookGen/Commands/Html2OpenXmlCommand.cs | 4 +- Source/BookGen/Commands/Html2PdfCommand.cs | 4 +- Source/BookGen/Commands/Html2PngCommand.cs | 8 +- Source/BookGen/Commands/ImgConvert.cs | 12 +- .../BookGen/Commands/InputOutputArguments.cs | 4 +- Source/BookGen/Commands/JsonArgsCommand.cs | 2 +- Source/BookGen/Commands/LinksCommand.cs | 2 +- Source/BookGen/Commands/Math2SvgCommand.cs | 6 +- Source/BookGen/Commands/Md2HtmlCommand.cs | 16 +- Source/BookGen/Commands/Md2TerminalCommand.cs | 4 +- Source/BookGen/Commands/NewPageCommand.cs | 2 +- Source/BookGen/Commands/QrCodeCommand.cs | 4 +- Source/BookGen/Commands/ShellCommand.cs | 11 +- Source/BookGen/Commands/SpellCheckCommand.cs | 8 +- Source/BookGen/Commands/TemplatesCommand.cs | 2 +- .../Commands/TerminalInstallCommand.cs | 4 +- .../AttachDebuggerParser.cs | 32 ++++ .../GlobalOptionParsers/JsonLogParser.cs | 27 ++++ .../GlobalOptionParsers/LogToFileParser.cs | 27 ++++ .../RuntimePrintingParser.cs | 28 ++++ .../GlobalOptionParsers/WaitDebuggerParser.cs | 46 ++++++ .../Infrastructure/ProgramConfigurator.cs | 131 --------------- Source/BookGen/Program.cs | 13 +- Test/Bookgen.Tests/Cli/UT_CommandRunner.cs | 2 +- 50 files changed, 1076 insertions(+), 200 deletions(-) create mode 100644 Source/BookGen.Cli/Annotations/ExitCodeAttribute.cs create mode 100644 Source/BookGen.Cli/IGlobalOptionParser.cs create mode 100644 Source/BookGen.Cli/OpenCli/Draft/Argument.cs create mode 100644 Source/BookGen.Cli/OpenCli/Draft/Arity.cs create mode 100644 Source/BookGen.Cli/OpenCli/Draft/CliInfo.cs create mode 100644 Source/BookGen.Cli/OpenCli/Draft/Command.cs create mode 100644 Source/BookGen.Cli/OpenCli/Draft/Contact.cs create mode 100644 Source/BookGen.Cli/OpenCli/Draft/Conventions.cs create mode 100644 Source/BookGen.Cli/OpenCli/Draft/Document.cs create mode 100644 Source/BookGen.Cli/OpenCli/Draft/ExitCode.cs create mode 100644 Source/BookGen.Cli/OpenCli/Draft/License.cs create mode 100644 Source/BookGen.Cli/OpenCli/Draft/Metadata.cs create mode 100644 Source/BookGen.Cli/OpenCli/Draft/Option.cs create mode 100644 Source/BookGen.Cli/OpenCli/OpenCliDraftGenerator.cs create mode 100644 Source/BookGen.Cli/ProgramMetaData.cs create mode 100644 Source/BookGen/GlobalOptionParsers/AttachDebuggerParser.cs create mode 100644 Source/BookGen/GlobalOptionParsers/JsonLogParser.cs create mode 100644 Source/BookGen/GlobalOptionParsers/LogToFileParser.cs create mode 100644 Source/BookGen/GlobalOptionParsers/RuntimePrintingParser.cs create mode 100644 Source/BookGen/GlobalOptionParsers/WaitDebuggerParser.cs delete mode 100644 Source/BookGen/Infrastructure/ProgramConfigurator.cs diff --git a/Source/BookGen.Cli/Annotations/ExitCodeAttribute.cs b/Source/BookGen.Cli/Annotations/ExitCodeAttribute.cs new file mode 100644 index 00000000..3ff69147 --- /dev/null +++ b/Source/BookGen.Cli/Annotations/ExitCodeAttribute.cs @@ -0,0 +1,16 @@ +namespace BookGen.Cli.Annotations; + + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +internal sealed class ExitCodeAttribute : Attribute +{ + public int ExitCode { get; } + public string Description { get; } + + public ExitCodeAttribute(int exitCode, string description) + { + ExitCode = exitCode; + Description = description; + } + +} diff --git a/Source/BookGen.Cli/Annotations/SwitchAttribute.cs b/Source/BookGen.Cli/Annotations/SwitchAttribute.cs index e985b74b..b1121ba8 100644 --- a/Source/BookGen.Cli/Annotations/SwitchAttribute.cs +++ b/Source/BookGen.Cli/Annotations/SwitchAttribute.cs @@ -10,10 +10,12 @@ public sealed class SwitchAttribute : Attribute { public string LongName { get; } public string ShortName { get; } + public bool Required { get; } - public SwitchAttribute(string shortName, string longName) + public SwitchAttribute(string shortName, string longName, bool required) { LongName = longName; ShortName = shortName; + Required = required; } } diff --git a/Source/BookGen.Cli/CommandRunner.cs b/Source/BookGen.Cli/CommandRunner.cs index 41948d53..b5aa250a 100644 --- a/Source/BookGen.Cli/CommandRunner.cs +++ b/Source/BookGen.Cli/CommandRunner.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -10,6 +10,8 @@ using BookGen.Cli.Annotations; using BookGen.Cli.ArgumentParsing; +using BookGen.Cli.OpenCli; +using BookGen.Cli.OpenCli.Draft; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -24,6 +26,7 @@ public sealed class CommandRunner private readonly ICommandHelpProvider _helpProvider; private readonly ILogger _log; private readonly CommandRunnerSettings _settings; + private readonly List _globalOptionParsers; private readonly SupportedOs _currentOs; private string? _defaultCommandName; @@ -126,7 +129,7 @@ private ICommand CreateCommand(string commandName) } public CommandRunner(IServiceProvider serviceProvider, - ICommandHelpProvider helpProvider, + ICommandHelpProvider helpProvider, ILogger log, CommandRunnerSettings settings) { @@ -136,6 +139,7 @@ public CommandRunner(IServiceProvider serviceProvider, AllowTrailingCommas = true, WriteIndented = true }; + _globalOptionParsers = new List(); _commands = new Dictionary(); _serviceProvider = serviceProvider; _helpProvider = helpProvider; @@ -158,6 +162,21 @@ private static void ConfigureUtfSupport(bool enableUtf8Output) } } + // Skip the first argument (command name) and any parsed global options + private static List GetArgsToParse(IReadOnlyList args, HashSet parsedGlobals) + { + List results = new(); + for (int i = 1; i < args.Count; i++) + { + if (!parsedGlobals.Contains(args[i])) + { + results.Add(args[i]); + } + } + return results; + } + + public Action ExceptionHandlerDelegate { get; set; } public CommandRunner AddCommand() where TCommand : ICommand @@ -178,6 +197,28 @@ public CommandRunner AddDefaultCommand() where TCommand : ICommand return this; } + public CommandRunner AddGlobalOptionParser(GlobalOptionParser parser) + { + _globalOptionParsers.Add(parser); + return this; + } + + public CommandRunner AddGlobalOptionParser() where TParser : GlobalOptionParser, new() + { + _globalOptionParsers.Add(new TParser()); + return this; + } + + public IEnumerable GetGlobalOptions() + { + foreach (GlobalOptionParser parser in _globalOptionParsers) + { + yield return parser.ShortName; + yield return parser.LongName; + } + } + + public CommandRunner AddCommandsFrom(Assembly assembly) { IEnumerable commands = assembly @@ -216,6 +257,20 @@ public string[] GetAutoCompleteItems(string commandName) return Array.Empty(); } + public Document GenerateOpenCliDocs() + { + if (string.IsNullOrEmpty(_defaultCommandName)) + throw new InvalidOperationException("Default command hasn't been set"); + + IEnumerable<(Type Value, Type?)> commands = _commands.Select(x => (x.Value, GetArgumentType(x.Value))); + + return OpenCliDraftGenerator.GenerateOpenCli(_settings.ProgramMetaData.AppName, + _settings.ProgramMetaData.Version, + _commands[_defaultCommandName], + _globalOptionParsers, + commands); + } + public async Task Run(IReadOnlyList args) { try @@ -233,7 +288,16 @@ public async Task Run(IReadOnlyList args) commandName = _defaultCommandName; } - var argsToParse = args.Skip(1).ToArray(); + HashSet parsedGlobals = new(); + foreach (var parser in _globalOptionParsers) + { + if (parser.TryParseGlobalOption(args.ToArray(), out string? globalOption)) + { + parsedGlobals.Add(globalOption); + } + } + + List argsToParse = GetArgsToParse(args, parsedGlobals); return await RunCommand(commandName, argsToParse); } diff --git a/Source/BookGen.Cli/CommandRunnerProxy.cs b/Source/BookGen.Cli/CommandRunnerProxy.cs index 821fc84a..c8bada03 100644 --- a/Source/BookGen.Cli/CommandRunnerProxy.cs +++ b/Source/BookGen.Cli/CommandRunnerProxy.cs @@ -1,20 +1,26 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- +using BookGen.Cli.OpenCli.Draft; + namespace BookGen.Cli; public sealed class CommandRunnerProxy : ICommandRunnerProxy { private Func? _autoComplete; private Func, Task>? _runCommand; + private Func? _openCliGenerator; + private IEnumerable? _globalOptions; public void ConfigureWith(CommandRunner runner) { CommandNames = runner.CommandNames; _autoComplete = runner.GetAutoCompleteItems; _runCommand = runner.RunCommand; + _openCliGenerator = runner.GenerateOpenCliDocs; + _globalOptions = runner.GetGlobalOptions(); } public IEnumerable CommandNames @@ -23,6 +29,10 @@ public IEnumerable CommandNames private set; } + public IEnumerable GlobalOptions + => _globalOptions + ?? throw new InvalidOperationException("Provider hasn't been setup correctly"); + public string[] GetAutoCompleteItems(string commandName) => _autoComplete?.Invoke(commandName) ?? throw new InvalidOperationException("Provider hasn't been setup correctly"); @@ -34,4 +44,8 @@ public async Task RunCommand(string commandName, IReadOnlyList args return await _runCommand.Invoke(commandName, argsToParse); } + + public Document GetOpenCliDocs() + => _openCliGenerator?.Invoke() + ?? throw new InvalidOperationException("Provider hasn't been setup correctly"); } diff --git a/Source/BookGen.Cli/CommandRunnerSettings.cs b/Source/BookGen.Cli/CommandRunnerSettings.cs index 18b9c848..0e75d3ab 100644 --- a/Source/BookGen.Cli/CommandRunnerSettings.cs +++ b/Source/BookGen.Cli/CommandRunnerSettings.cs @@ -13,6 +13,7 @@ public sealed record class CommandRunnerSettings public required int ExcptionExitCode { get; init; } public required bool EnableUtf8Output { get; init; } public required bool PrintHelpOnBadArgs { get; init; } + public required ProgramMetaData ProgramMetaData { get; init; } public static readonly CommandRunnerSettings Default = new() { @@ -22,5 +23,10 @@ public sealed record class CommandRunnerSettings ExcptionExitCode = -4, EnableUtf8Output = true, PrintHelpOnBadArgs = false, + ProgramMetaData = new ProgramMetaData + { + AppName = "", + Version = new Version() + } }; } diff --git a/Source/BookGen.Cli/ICommandRunnerProxy.cs b/Source/BookGen.Cli/ICommandRunnerProxy.cs index efd7599c..cc8114da 100644 --- a/Source/BookGen.Cli/ICommandRunnerProxy.cs +++ b/Source/BookGen.Cli/ICommandRunnerProxy.cs @@ -1,14 +1,17 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- +using BookGen.Cli.OpenCli.Draft; + namespace BookGen.Cli; public interface ICommandRunnerProxy { IEnumerable CommandNames { get; } - + IEnumerable GlobalOptions { get; } string[] GetAutoCompleteItems(string commandName); Task RunCommand(string commandName, IReadOnlyList argsToParse); + Document GetOpenCliDocs(); } diff --git a/Source/BookGen.Cli/IGlobalOptionParser.cs b/Source/BookGen.Cli/IGlobalOptionParser.cs new file mode 100644 index 00000000..647d8c39 --- /dev/null +++ b/Source/BookGen.Cli/IGlobalOptionParser.cs @@ -0,0 +1,51 @@ +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using System.Diagnostics.CodeAnalysis; + +namespace BookGen.Cli; + +public abstract class GlobalOptionParser +{ + public string LongName { get; } + public string ShortName { get; } + + public GlobalOptionParser(string shortName, string longName) + { + if (longName.StartsWith('-')) + throw new ArgumentException("Long name cannot start with '--'", nameof(longName)); + + if (shortName.StartsWith('-')) + throw new ArgumentException("Short name cannot start with '-'", nameof(shortName)); + + ShortName = $"-{shortName}"; + LongName = $"--{longName}"; + } + + public bool TryParseGlobalOption(string[] args, [NotNullWhen(true)] out string? parsedOne) + { + bool handle = false; + parsedOne = null; + if (args.Contains(ShortName)) + { + handle = true; + parsedOne = ShortName; + } + else if (args.Contains(LongName)) + { + handle = true; + parsedOne = LongName; + } + + if (handle) + { + OnOptionWasPresent(); + } + + return handle; + } + + protected abstract void OnOptionWasPresent(); +} diff --git a/Source/BookGen.Cli/OpenCli/Draft/Argument.cs b/Source/BookGen.Cli/OpenCli/Draft/Argument.cs new file mode 100644 index 00000000..a631820e --- /dev/null +++ b/Source/BookGen.Cli/OpenCli/Draft/Argument.cs @@ -0,0 +1,66 @@ +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using System.Text.Json.Serialization; + +namespace BookGen.Cli.OpenCli.Draft; + +public sealed class Argument +{ + /// + /// A list of accepted values + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("acceptedValues")] + public List? AcceptedValues { get; set; } + + /// + /// The argument arity. Arity defines the minimum and maximum number of argument values + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("arity")] + public Arity? Arity { get; set; } + + /// + /// The argument description + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// + /// The argument group + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("group")] + public string? Group { get; set; } + + /// + /// Whether or not the argument is hidden + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("hidden")] + public bool? Hidden { get; set; } + + /// + /// Custom metadata + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("metadata")] + public List? Metadata { get; set; } + + /// + /// The argument name + /// + [JsonPropertyName("name")] + public required string Name { get; set; } + + /// + /// Whether or not the argument is required + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("required")] + public bool? OpenClRequired { get; set; } +} diff --git a/Source/BookGen.Cli/OpenCli/Draft/Arity.cs b/Source/BookGen.Cli/OpenCli/Draft/Arity.cs new file mode 100644 index 00000000..a4d2c15d --- /dev/null +++ b/Source/BookGen.Cli/OpenCli/Draft/Arity.cs @@ -0,0 +1,48 @@ +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using System.Text.Json.Serialization; + +namespace BookGen.Cli.OpenCli.Draft; + +/// +/// The argument arity. Arity defines the minimum and maximum number of argument values +/// +public partial class Arity +{ + /// + /// The maximum number of values allowed + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("maximum")] + public long? Maximum + { + get => field; + set + { + if (value < Minimum) + throw new ArgumentOutOfRangeException(nameof(Maximum)); + + field = value; + } + } + + /// + /// The minimum number of values allowed + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("minimum")] + public long Minimum + { + get => field; + set + { + if (value < 1) + throw new ArgumentOutOfRangeException(nameof(Minimum)); + + field = value; + } + } = 1; +} diff --git a/Source/BookGen.Cli/OpenCli/Draft/CliInfo.cs b/Source/BookGen.Cli/OpenCli/Draft/CliInfo.cs new file mode 100644 index 00000000..be8bbfb3 --- /dev/null +++ b/Source/BookGen.Cli/OpenCli/Draft/CliInfo.cs @@ -0,0 +1,54 @@ +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using System.Text.Json.Serialization; + +namespace BookGen.Cli.OpenCli.Draft; + +/// +/// Information about the CLI +/// +public sealed class CliInfo +{ + /// + /// The contact information + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("contact")] + public Contact? Contact { get; set; } + + /// + /// A description of the application + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// + /// The application license + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("license")] + public License? License { get; set; } + + /// + /// A short summary of the application + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("summary")] + public string? Summary { get; set; } + + /// + /// The application title + /// + [JsonPropertyName("title")] + public required string Title { get; set; } + + /// + /// The application version + /// + [JsonPropertyName("version")] + public required string Version { get; set; } +} diff --git a/Source/BookGen.Cli/OpenCli/Draft/Command.cs b/Source/BookGen.Cli/OpenCli/Draft/Command.cs new file mode 100644 index 00000000..89a2892c --- /dev/null +++ b/Source/BookGen.Cli/OpenCli/Draft/Command.cs @@ -0,0 +1,90 @@ +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using System.Text.Json.Serialization; + +namespace BookGen.Cli.OpenCli.Draft; + +/// +/// The root command +/// +public sealed class Command +{ + /// + /// The command aliases + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("aliases")] + public List? Aliases { get; set; } + + /// + /// The command arguments + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("arguments")] + public List? Arguments { get; set; } + + /// + /// The command's sub commands + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("commands")] + public List? Commands { get; set; } + + /// + /// The command description + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// + /// Examples of how to use the command + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("examples")] + public List? Examples { get; set; } + + /// + /// The command's exit codes + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("exitCodes")] + public List? ExitCodes { get; set; } + + /// + /// Whether or not the command is hidden + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("hidden")] + public bool? Hidden { get; set; } + + /// + /// Indicate whether or not the command requires interactive input + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("interactive")] + public bool? Interactive { get; set; } + + /// + /// Custom metadata + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("metadata")] + public List? Metadata { get; set; } + + /// + /// The command name + /// + [JsonPropertyName("name")] + public required string Name { get; set; } + + /// + /// The command options + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("options")] + public List