From 353c7b0108679e5a10982a7db399d7b6e563d424 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Wed, 8 May 2024 20:33:01 +1000 Subject: [PATCH] Hide preview commands behind the --pre flag as Seq does --- src/SeqCli/Cli/CommandAttribute.cs | 2 +- src/SeqCli/Cli/CommandLineHost.cs | 18 +++++--- src/SeqCli/Cli/CommandMetadata.cs | 3 +- .../Cli/Commands/Forwarder/InstallCommand.cs | 2 +- .../Cli/Commands/Forwarder/RestartCommand.cs | 2 +- .../Cli/Commands/Forwarder/RunCommand.cs | 2 +- .../Cli/Commands/Forwarder/StartCommand.cs | 2 +- .../Cli/Commands/Forwarder/StatusCommand.cs | 2 +- .../Cli/Commands/Forwarder/StopCommand.cs | 2 +- .../Cli/Commands/Forwarder/TruncateCommand.cs | 2 +- .../Commands/Forwarder/UninstallCommand.cs | 2 +- src/SeqCli/Cli/Commands/HelpCommand.cs | 38 ++++++++++------- .../Help/MarkdownHelpTestCase.cs | 2 + test/SeqCli.Tests/Cli/CommandLineHostTests.cs | 41 ++++++++++++------- test/SeqCli.Tests/Support/ActionCommand.cs | 12 ++++++ 15 files changed, 87 insertions(+), 45 deletions(-) create mode 100644 test/SeqCli.Tests/Support/ActionCommand.cs diff --git a/src/SeqCli/Cli/CommandAttribute.cs b/src/SeqCli/Cli/CommandAttribute.cs index 775e03a5..a41f5422 100644 --- a/src/SeqCli/Cli/CommandAttribute.cs +++ b/src/SeqCli/Cli/CommandAttribute.cs @@ -22,8 +22,8 @@ public class CommandAttribute : Attribute, ICommandMetadata public string Name { get; } public string? SubCommand { get; } public string HelpText { get; } - public string? Example { get; set; } + public bool IsPreview { get; set; } public CommandAttribute(string name, string helpText) { diff --git a/src/SeqCli/Cli/CommandLineHost.cs b/src/SeqCli/Cli/CommandLineHost.cs index e5c686fc..407282ef 100644 --- a/src/SeqCli/Cli/CommandLineHost.cs +++ b/src/SeqCli/Cli/CommandLineHost.cs @@ -39,19 +39,25 @@ public async Task Run(string[] args, LoggingLevelSwitch levelSwitch) if (args.Length > 0) { + const string prereleaseArg = "--pre", verboseArg = "--verbose"; + var norm = args[0].ToLowerInvariant(); var subCommandNorm = args.Length > 1 && !args[1].Contains('-') ? args[1].ToLowerInvariant() : null; - + + var pre = args.Any(a => a == prereleaseArg); + var cmd = _availableCommands.SingleOrDefault(c => - c.Metadata.Name == norm && (c.Metadata.SubCommand == subCommandNorm || c.Metadata.SubCommand == null)); + (!c.Metadata.IsPreview || pre) && + c.Metadata.Name == norm && + (c.Metadata.SubCommand == subCommandNorm || c.Metadata.SubCommand == null)); if (cmd != null) { var amountToSkip = cmd.Metadata.SubCommand == null ? 1 : 2; - var commandSpecificArgs = args.Skip(amountToSkip).ToArray(); + var commandSpecificArgs = args.Skip(amountToSkip).Where(arg => arg != prereleaseArg).ToArray(); - var verboseArg = commandSpecificArgs.FirstOrDefault(arg => arg == "--verbose"); - if (verboseArg != null) + var verbose = commandSpecificArgs.Any(arg => arg == verboseArg); + if (verbose) { levelSwitch.MinimumLevel = LogEventLevel.Information; commandSpecificArgs = commandSpecificArgs.Where(arg => arg != verboseArg).ToArray(); @@ -63,6 +69,6 @@ public async Task Run(string[] args, LoggingLevelSwitch levelSwitch) Console.WriteLine($"Usage: {name} []"); Console.WriteLine($"Type `{name} help` for available commands"); - return -1; + return 1; } } \ No newline at end of file diff --git a/src/SeqCli/Cli/CommandMetadata.cs b/src/SeqCli/Cli/CommandMetadata.cs index e691cce7..49b18d89 100644 --- a/src/SeqCli/Cli/CommandMetadata.cs +++ b/src/SeqCli/Cli/CommandMetadata.cs @@ -20,4 +20,5 @@ public class CommandMetadata : ICommandMetadata public string? SubCommand { get; set; } public string HelpText { get; set; } = null!; public string? Example { get; set; } -} \ No newline at end of file + public bool IsPreview { get; set; } +} diff --git a/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs b/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs index 7d709715..b3bb1c3d 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs @@ -35,7 +35,7 @@ namespace SeqCli.Forwarder.Cli.Commands { - [Command("forwarder", "install", "Install the forwarder as a Windows service")] + [Command("forwarder", "install", "Install the forwarder as a Windows service", IsPreview = true)] [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] class InstallCommand : Command { diff --git a/src/SeqCli/Cli/Commands/Forwarder/RestartCommand.cs b/src/SeqCli/Cli/Commands/Forwarder/RestartCommand.cs index 63008cb0..65a6a6b9 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/RestartCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/RestartCommand.cs @@ -26,7 +26,7 @@ namespace SeqCli.Forwarder.Cli.Commands { - [Command("forwarder", "restart", "Restart the forwarder Windows service")] + [Command("forwarder", "restart", "Restart the forwarder Windows service", IsPreview = true)] [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] class RestartCommand : Command { diff --git a/src/SeqCli/Cli/Commands/Forwarder/RunCommand.cs b/src/SeqCli/Cli/Commands/Forwarder/RunCommand.cs index 9a2a1b32..4e69cebd 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/RunCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/RunCommand.cs @@ -45,7 +45,7 @@ namespace SeqCli.Cli.Commands.Forwarder; -[Command("forwarder", "run", "Listen on an HTTP endpoint and forward ingested logs to Seq")] +[Command("forwarder", "run", "Listen on an HTTP endpoint and forward ingested logs to Seq", IsPreview = true)] class RunCommand : Command { readonly StoragePathFeature _storagePath; diff --git a/src/SeqCli/Cli/Commands/Forwarder/StartCommand.cs b/src/SeqCli/Cli/Commands/Forwarder/StartCommand.cs index 98ee92f8..66f859fc 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/StartCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/StartCommand.cs @@ -24,7 +24,7 @@ namespace SeqCli.Forwarder.Cli.Commands { - [Command("forwarder", "start", "Start the forwarder Windows service")] + [Command("forwarder", "start", "Start the forwarder Windows service", IsPreview = true)] [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] class StartCommand : Command { diff --git a/src/SeqCli/Cli/Commands/Forwarder/StatusCommand.cs b/src/SeqCli/Cli/Commands/Forwarder/StatusCommand.cs index 3d0073b1..cd5fa6f6 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/StatusCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/StatusCommand.cs @@ -24,7 +24,7 @@ namespace SeqCli.Forwarder.Cli.Commands { - [Command("forwarder", "status", "Show the status of the forwarder Windows service")] + [Command("forwarder", "status", "Show the status of the forwarder Windows service", IsPreview = true)] [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] class StatusCommand : Command { diff --git a/src/SeqCli/Cli/Commands/Forwarder/StopCommand.cs b/src/SeqCli/Cli/Commands/Forwarder/StopCommand.cs index 88d7db6b..955c550c 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/StopCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/StopCommand.cs @@ -24,7 +24,7 @@ namespace SeqCli.Forwarder.Cli.Commands { - [Command("forwarder", "stop", "Stop the forwarder Windows service")] + [Command("forwarder", "stop", "Stop the forwarder Windows service", IsPreview = true)] [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] class StopCommand : Command { diff --git a/src/SeqCli/Cli/Commands/Forwarder/TruncateCommand.cs b/src/SeqCli/Cli/Commands/Forwarder/TruncateCommand.cs index 844eb9ee..bf50a2ae 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/TruncateCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/TruncateCommand.cs @@ -19,7 +19,7 @@ namespace SeqCli.Cli.Commands.Forwarder; -[Command("forwarder", "truncate", "Empty the forwarder's persistent log buffer")] +[Command("forwarder", "truncate", "Empty the forwarder's persistent log buffer", IsPreview = true)] class TruncateCommand : Command { readonly StoragePathFeature _storagePath; diff --git a/src/SeqCli/Cli/Commands/Forwarder/UninstallCommand.cs b/src/SeqCli/Cli/Commands/Forwarder/UninstallCommand.cs index 224cc94c..20bfbcdd 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/UninstallCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/UninstallCommand.cs @@ -24,7 +24,7 @@ namespace SeqCli.Forwarder.Cli.Commands { - [Command("forwarder", "uninstall", "Uninstall the forwarder Windows service")] + [Command("forwarder", "uninstall", "Uninstall the forwarder Windows service", IsPreview = true)] class UninstallCommand : Command { protected override Task Run() diff --git a/src/SeqCli/Cli/Commands/HelpCommand.cs b/src/SeqCli/Cli/Commands/HelpCommand.cs index 026cfcac..3aab532b 100644 --- a/src/SeqCli/Cli/Commands/HelpCommand.cs +++ b/src/SeqCli/Cli/Commands/HelpCommand.cs @@ -18,23 +18,31 @@ using System.Reflection; using System.Threading.Tasks; using Autofac.Features.Metadata; +using CommandList = System.Collections.Generic.List, SeqCli.Cli.CommandMetadata>>; namespace SeqCli.Cli.Commands; [Command("help", "Show information about available commands", Example = "seqcli help search")] class HelpCommand : Command { - readonly List, CommandMetadata>> _orderedCommands; - bool _markdown; + readonly IEnumerable, CommandMetadata>> _availableCommands; + bool _markdown, _pre; public HelpCommand(IEnumerable, CommandMetadata>> availableCommands) { + _availableCommands = availableCommands; + Options.Add("pre", "Show preview commands", _ => _pre = true); Options.Add("m|markdown", "Generate markdown for use in documentation", _ => _markdown = true); - _orderedCommands = availableCommands.OrderBy(c => c.Metadata.Name).ThenBy(c => c.Metadata.SubCommand).ToList(); } protected override Task Run(string[] unrecognized) { + var orderedCommands = _availableCommands + .Where(c => !c.Metadata.IsPreview || _pre) + .OrderBy(c => c.Metadata.Name) + .ThenBy(c => c.Metadata.SubCommand) + .ToList(); + var ea = Assembly.GetEntryAssembly(); // ReSharper disable once PossibleNullReferenceException var name = ea!.GetName().Name!; @@ -44,7 +52,7 @@ protected override Task Run(string[] unrecognized) if (unrecognized.Length != 0) return base.Run(unrecognized); - PrintMarkdownHelp(name); + PrintMarkdownHelp(name, orderedCommands); return Task.FromResult(0); } @@ -53,7 +61,7 @@ protected override Task Run(string[] unrecognized) { topLevelCommand = unrecognized[0].ToLowerInvariant(); var subCommand = unrecognized.Length > 1 && !unrecognized[1].Contains("-") ? unrecognized[1] : null; - var cmds = _orderedCommands.Where(c => c.Metadata.Name == topLevelCommand && + var cmds = orderedCommands.Where(c => c.Metadata.Name == topLevelCommand && (subCommand == null || subCommand == c.Metadata.SubCommand)).ToArray(); if (cmds.Length == 1 && cmds[0].Metadata.SubCommand == subCommand) @@ -79,15 +87,15 @@ protected override Task Run(string[] unrecognized) } } - if (topLevelCommand != null && _orderedCommands.Any(a => a.Metadata.Name == topLevelCommand)) - PrintHelp(name, topLevelCommand); + if (topLevelCommand != null && orderedCommands.Any(a => a.Metadata.Name == topLevelCommand)) + PrintHelp(name, topLevelCommand, orderedCommands); else - PrintHelp(name); + PrintHelp(name, orderedCommands); return Task.FromResult(0); } - void PrintMarkdownHelp(string executableName) + static void PrintMarkdownHelp(string executableName, CommandList orderedCommands) { Console.WriteLine("## Commands"); Console.WriteLine(); @@ -101,7 +109,7 @@ void PrintMarkdownHelp(string executableName) Console.WriteLine("Available commands:"); Console.WriteLine(); - foreach (var cmd in _orderedCommands.GroupBy(cmd => cmd.Metadata.Name).OrderBy(c => c.Key)) + foreach (var cmd in orderedCommands.GroupBy(cmd => cmd.Metadata.Name).OrderBy(c => c.Key)) { if (cmd.Count() == 1) { @@ -122,7 +130,7 @@ void PrintMarkdownHelp(string executableName) } Console.WriteLine(); - foreach (var cmd in _orderedCommands) + foreach (var cmd in orderedCommands) { if (cmd.Metadata.SubCommand != null) Console.WriteLine($"### `{cmd.Metadata.Name} {cmd.Metadata.SubCommand}`"); @@ -166,14 +174,14 @@ void PrintMarkdownHelp(string executableName) } } - void PrintHelp(string executableName) + static void PrintHelp(string executableName, CommandList orderedCommands) { Console.WriteLine($"Usage: {executableName} []"); Console.WriteLine(); Console.WriteLine("Available commands are:"); var printedGroups = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (var avail in _orderedCommands) + foreach (var avail in orderedCommands) { if (avail.Metadata.SubCommand != null) { @@ -193,13 +201,13 @@ void PrintHelp(string executableName) Console.WriteLine($"Type `{executableName} help ` for detailed help"); } - void PrintHelp(string executableName, string topLevelCommand) + static void PrintHelp(string executableName, string topLevelCommand, CommandList orderedCommands) { Console.WriteLine($"Usage: {executableName} {topLevelCommand} []"); Console.WriteLine(); Console.WriteLine("Available sub-commands are:"); - foreach (var avail in _orderedCommands.Where(c => c.Metadata.Name == topLevelCommand)) + foreach (var avail in orderedCommands.Where(c => c.Metadata.Name == topLevelCommand)) { Printing.Define($" {avail.Metadata.SubCommand}", avail.Metadata.HelpText, Console.Out); } diff --git a/test/SeqCli.EndToEnd/Help/MarkdownHelpTestCase.cs b/test/SeqCli.EndToEnd/Help/MarkdownHelpTestCase.cs index bfecf1ff..9177492b 100644 --- a/test/SeqCli.EndToEnd/Help/MarkdownHelpTestCase.cs +++ b/test/SeqCli.EndToEnd/Help/MarkdownHelpTestCase.cs @@ -24,6 +24,8 @@ public Task ExecuteAsync(SeqConnection connection, ILogger logger, CliCommandRun var indexOfTemplateImport = markdown.IndexOf("### `template import`", StringComparison.Ordinal); Assert.NotEqual(indexOfTemplateExport, indexOfTemplateImport); Assert.True(indexOfTemplateExport < indexOfTemplateImport); + + Assert.DoesNotContain("### `forwarder run`", markdown); return Task.CompletedTask; } diff --git a/test/SeqCli.Tests/Cli/CommandLineHostTests.cs b/test/SeqCli.Tests/Cli/CommandLineHostTests.cs index 78bea1e3..ee72b1ef 100644 --- a/test/SeqCli.Tests/Cli/CommandLineHostTests.cs +++ b/test/SeqCli.Tests/Cli/CommandLineHostTests.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Autofac.Features.Metadata; using SeqCli.Cli; +using SeqCli.Tests.Support; using Serilog.Core; using Serilog.Events; using Xunit; @@ -13,7 +14,7 @@ namespace SeqCli.Tests.Cli; public class CommandLineHostTests { [Fact] - public async Task CheckCommandLineHostPicksCorrectCommand() + public async Task CommandLineHostPicksCorrectCommand() { var executed = new List(); var availableCommands = new List, CommandMetadata>> @@ -28,27 +29,47 @@ public async Task CheckCommandLineHostPicksCorrectCommand() var commandLineHost = new CommandLineHost(availableCommands); await commandLineHost.Run(["test"],new LoggingLevelSwitch()); - Assert.Equal("test", executed.First()); + Assert.Equal("test", executed.Single()); + } + + [Fact] + public async Task PrereleaseCommandsAreIgnoredWithoutFlag() + { + var executed = new List(); + var availableCommands = new List, CommandMetadata>> + { + new( + new Lazy(() => new ActionCommand(() => executed.Add("test"))), + new CommandMetadata {Name = "test", IsPreview = true}), + }; + var commandLineHost = new CommandLineHost(availableCommands); + var exit = await commandLineHost.Run(["test"],new LoggingLevelSwitch()); + Assert.Equal(1, exit); + Assert.Empty(executed); + + exit = await commandLineHost.Run(["test", "--pre"],new LoggingLevelSwitch()); + Assert.Equal(0, exit); + Assert.Equal("test", executed.Single()); } [Fact] public async Task WhenMoreThanOneSubcommandAndTheUserRunsWithSubcommandEnsurePickedCorrect() { - var commandsRan = new List(); + var executed = new List(); var availableCommands = new List, CommandMetadata>> { new( - new Lazy(() => new ActionCommand(() => commandsRan.Add("test-subcommand1"))), + new Lazy(() => new ActionCommand(() => executed.Add("test-subcommand1"))), new CommandMetadata {Name = "test", SubCommand = "subcommand1"}), new( - new Lazy(() => new ActionCommand(() => commandsRan.Add("test-subcommand2"))), + new Lazy(() => new ActionCommand(() => executed.Add("test-subcommand2"))), new CommandMetadata {Name = "test", SubCommand = "subcommand2"}) }; var commandLineHost = new CommandLineHost(availableCommands); await commandLineHost.Run(["test", "subcommand2"], new LoggingLevelSwitch()); - Assert.Equal("test-subcommand2", commandsRan.First()); + Assert.Equal("test-subcommand2", executed.First()); } [Fact] @@ -70,12 +91,4 @@ public async Task VerboseOptionSetsLoggingLevelToInformation() Assert.Equal(LogEventLevel.Information, levelSwitch.MinimumLevel); } - - class ActionCommand : Command - { - public ActionCommand(Action action) - { - action.Invoke(); - } - } } \ No newline at end of file diff --git a/test/SeqCli.Tests/Support/ActionCommand.cs b/test/SeqCli.Tests/Support/ActionCommand.cs new file mode 100644 index 00000000..bf7ae4c9 --- /dev/null +++ b/test/SeqCli.Tests/Support/ActionCommand.cs @@ -0,0 +1,12 @@ +using System; +using SeqCli.Cli; + +namespace SeqCli.Tests.Support; + +class ActionCommand : Command +{ + public ActionCommand(Action action) + { + action.Invoke(); + } +} \ No newline at end of file