diff --git a/src/SeqCli/Cli/CommandAttribute.cs b/src/SeqCli/Cli/CommandAttribute.cs index a41f5422..95aaf184 100644 --- a/src/SeqCli/Cli/CommandAttribute.cs +++ b/src/SeqCli/Cli/CommandAttribute.cs @@ -23,12 +23,13 @@ public class CommandAttribute : Attribute, ICommandMetadata public string? SubCommand { get; } public string HelpText { get; } public string? Example { get; set; } - public bool IsPreview { get; set; } + public FeatureVisibility Visibility { get; set; } public CommandAttribute(string name, string helpText) { Name = name; HelpText = helpText; + Visibility = FeatureVisibility.Visible; } public CommandAttribute(string name, string subCommand, string helpText) : this(name, helpText) diff --git a/src/SeqCli/Cli/CommandFeature.cs b/src/SeqCli/Cli/CommandFeature.cs index e06e8e92..80997d58 100644 --- a/src/SeqCli/Cli/CommandFeature.cs +++ b/src/SeqCli/Cli/CommandFeature.cs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using System.Collections.Generic; namespace SeqCli.Cli; @@ -21,5 +20,5 @@ abstract class CommandFeature { public abstract void Enable(OptionSet options); - public virtual IEnumerable GetUsageErrors() => Array.Empty(); + public virtual IEnumerable GetUsageErrors() => []; } \ No newline at end of file diff --git a/src/SeqCli/Cli/CommandLineHost.cs b/src/SeqCli/Cli/CommandLineHost.cs index 27d46fc8..7bf7d661 100644 --- a/src/SeqCli/Cli/CommandLineHost.cs +++ b/src/SeqCli/Cli/CommandLineHost.cs @@ -41,21 +41,30 @@ public async Task Run(string[] args, LoggingLevelSwitch levelSwitch) { const string prereleaseArg = "--pre", verboseArg = "--verbose"; - var norm = args[0].ToLowerInvariant(); - var subCommandNorm = args.Length > 1 && !args[1].Contains('-') ? args[1].ToLowerInvariant() : null; + var commandName = args[0].ToLowerInvariant(); + var subCommandName = args.Length > 1 && !args[1].Contains('-') ? args[1].ToLowerInvariant() : null; - var pre = args.Any(a => a == prereleaseArg); + var hiddenLegacyCommand = false; + if (subCommandName == null && commandName == "config") + { + hiddenLegacyCommand = true; + subCommandName = "legacy"; + } + + var featureVisibility = FeatureVisibility.Visible | FeatureVisibility.Hidden; + if (args.Any(a => a.Trim() is prereleaseArg)) + featureVisibility |= FeatureVisibility.Preview; var cmd = _availableCommands.SingleOrDefault(c => - (!c.Metadata.IsPreview || pre) && - c.Metadata.Name == norm && - (c.Metadata.SubCommand == subCommandNorm || c.Metadata.SubCommand == null)); + featureVisibility.HasFlag(c.Metadata.Visibility) && + c.Metadata.Name == commandName && + (c.Metadata.SubCommand == subCommandName || c.Metadata.SubCommand == null)); if (cmd != null) { - var amountToSkip = cmd.Metadata.SubCommand == null ? 1 : 2; - var commandSpecificArgs = args.Skip(amountToSkip).Where(arg => cmd.Metadata.Name == "help" || arg != prereleaseArg).ToArray(); - + var amountToSkip = cmd.Metadata.SubCommand == null || hiddenLegacyCommand ? 1 : 2; + var commandSpecificArgs = args.Skip(amountToSkip).Where(arg => cmd.Metadata.Name == "help" || arg is not prereleaseArg).ToArray(); + var verbose = commandSpecificArgs.Any(arg => arg == verboseArg); if (verbose) { diff --git a/src/SeqCli/Cli/CommandMetadata.cs b/src/SeqCli/Cli/CommandMetadata.cs index 997c450a..e062236b 100644 --- a/src/SeqCli/Cli/CommandMetadata.cs +++ b/src/SeqCli/Cli/CommandMetadata.cs @@ -20,5 +20,5 @@ public class CommandMetadata : ICommandMetadata public string? SubCommand { get; set; } public required string HelpText { get; set; } public string? Example { get; set; } - public bool IsPreview { get; set; } + public FeatureVisibility Visibility { get; set; } } diff --git a/src/SeqCli/Cli/Commands/Config/ClearCommand.cs b/src/SeqCli/Cli/Commands/Config/ClearCommand.cs new file mode 100644 index 00000000..d75cc6d8 --- /dev/null +++ b/src/SeqCli/Cli/Commands/Config/ClearCommand.cs @@ -0,0 +1,43 @@ +// Copyright © Datalust Pty Ltd and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Threading.Tasks; +using SeqCli.Cli.Features; +using SeqCli.Config; + +// ReSharper disable once UnusedType.Global + +namespace SeqCli.Cli.Commands.Config; + +[Command("config", "clear", "Clear fields in the `SeqCli.json` file")] +class ClearCommand : Command +{ + readonly StoragePathFeature _storagePath; + readonly ConfigKeyFeature _key; + + public ClearCommand() + { + _storagePath = Enable(); + _key = Enable(); + } + + protected override Task Run() + { + var config = SeqCliConfig.ReadFromFile(_storagePath.ConfigFilePath); + + KeyValueSettings.Clear(config, _key.GetKey()); + SeqCliConfig.WriteToFile(config, _storagePath.ConfigFilePath); + return Task.FromResult(0); + } +} diff --git a/src/SeqCli/Cli/Commands/Config/GetCommand.cs b/src/SeqCli/Cli/Commands/Config/GetCommand.cs new file mode 100644 index 00000000..f19bad16 --- /dev/null +++ b/src/SeqCli/Cli/Commands/Config/GetCommand.cs @@ -0,0 +1,45 @@ +// Copyright © Datalust Pty Ltd and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Threading.Tasks; +using SeqCli.Cli.Features; +using SeqCli.Config; + +// ReSharper disable once UnusedType.Global + +namespace SeqCli.Cli.Commands.Config; + +[Command("config", "get", "View a field from the `SeqCli.json` file")] +class GetCommand : Command +{ + readonly StoragePathFeature _storagePath; + readonly ConfigKeyFeature _key; + + public GetCommand() + { + _storagePath = Enable(); + _key = Enable(); + } + + protected override Task Run() + { + var config = SeqCliConfig.ReadFromFile(_storagePath.ConfigFilePath); + if (!KeyValueSettings.TryGetValue(config, _key.GetKey(), out var value, out _)) + throw new ArgumentException($"Field `{_key.GetKey()}` not found."); + + Console.WriteLine(value); + return Task.FromResult(0); + } +} \ No newline at end of file diff --git a/src/SeqCli/Cli/Commands/ConfigCommand.cs b/src/SeqCli/Cli/Commands/Config/LegacyCommand.cs similarity index 92% rename from src/SeqCli/Cli/Commands/ConfigCommand.cs rename to src/SeqCli/Cli/Commands/Config/LegacyCommand.cs index 94d9b0cb..a23b46bc 100644 --- a/src/SeqCli/Cli/Commands/ConfigCommand.cs +++ b/src/SeqCli/Cli/Commands/Config/LegacyCommand.cs @@ -19,16 +19,16 @@ using SeqCli.Util; using Serilog; -namespace SeqCli.Cli.Commands; +namespace SeqCli.Cli.Commands.Config; -[Command("config", "View and set fields in `SeqCli.json`; run with no arguments to list all fields")] -class ConfigCommand : Command +[Command("config", "legacy", "View and set fields in `SeqCli.json`; run with no arguments to list all fields", Visibility = FeatureVisibility.Hidden)] +class LegacyCommand : Command { string? _key, _value; bool _clear; readonly StoragePathFeature _storagePath; - public ConfigCommand() + public LegacyCommand() { Options.Add("k|key=", "The field, for example `connection.serverUrl`", k => _key = k); Options.Add("v|value=", "The field value; if not specified, the command will print the current value", v => _value = v); diff --git a/src/SeqCli/Cli/Commands/Config/ListCommand.cs b/src/SeqCli/Cli/Commands/Config/ListCommand.cs new file mode 100644 index 00000000..8c5eb557 --- /dev/null +++ b/src/SeqCli/Cli/Commands/Config/ListCommand.cs @@ -0,0 +1,43 @@ +// Copyright © Datalust Pty Ltd and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Threading.Tasks; +using SeqCli.Cli.Features; +using SeqCli.Config; + +// ReSharper disable once UnusedType.Global + +namespace SeqCli.Cli.Commands.Config; + +[Command("config", "list", "View all fields in the `SeqCli.json` file")] +class ListCommand : Command +{ + readonly StoragePathFeature _storagePath; + + public ListCommand() + { + _storagePath = Enable(); + } + + protected override Task Run() + { + var config = SeqCliConfig.ReadFromFile(_storagePath.ConfigFilePath); + foreach (var (key, value, _) in KeyValueSettings.Inspect(config)) + { + Console.WriteLine($"{key}={value}"); + } + return Task.FromResult(0); + } +} \ No newline at end of file diff --git a/src/SeqCli/Cli/Commands/Config/SetCommand.cs b/src/SeqCli/Cli/Commands/Config/SetCommand.cs new file mode 100644 index 00000000..da055ff5 --- /dev/null +++ b/src/SeqCli/Cli/Commands/Config/SetCommand.cs @@ -0,0 +1,48 @@ +// Copyright © Datalust Pty Ltd and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.IO; +using System.Threading.Tasks; +using SeqCli.Cli.Features; +using SeqCli.Config; +using Serilog; + +// ReSharper disable once UnusedType.Global + +namespace SeqCli.Cli.Commands.Config; + +[Command("config", "set", "Set a field in the `SeqCli.json` file")] +class SetCommand : Command +{ + readonly StoragePathFeature _storagePath; + readonly ConfigKeyFeature _key; + readonly ConfigValueFeature _value; + + public SetCommand() + { + _storagePath = Enable(); + _key = Enable(); + _value = Enable(); + } + + protected override Task Run() + { + var config = SeqCliConfig.ReadFromFile(_storagePath.ConfigFilePath); + + KeyValueSettings.Set(config, _key.GetKey(), _value.ReadValue()); + SeqCliConfig.WriteToFile(config, _storagePath.ConfigFilePath); + return Task.FromResult(0); + } +} \ No newline at end of file diff --git a/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs b/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs index 03d6d32e..d3452bfe 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs @@ -31,7 +31,7 @@ namespace SeqCli.Cli.Commands.Forwarder { - [Command("forwarder", "install", "Install the forwarder as a Windows service", IsPreview = true)] + [Command("forwarder", "install", "Install the forwarder as a Windows service", Visibility = FeatureVisibility.Preview)] [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 ef028c90..467ce1e1 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/RestartCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/RestartCommand.cs @@ -24,7 +24,7 @@ namespace SeqCli.Cli.Commands.Forwarder; -[Command("forwarder", "restart", "Restart the forwarder Windows service", IsPreview = true)] +[Command("forwarder", "restart", "Restart the forwarder Windows service", Visibility = FeatureVisibility.Preview)] [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 98a60764..ad0816ef 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", IsPreview = true)] +[Command("forwarder", "run", "Listen on an HTTP endpoint and forward ingested logs to Seq", Visibility = FeatureVisibility.Preview)] 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 0d6d0a0b..1eb7fb80 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/StartCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/StartCommand.cs @@ -22,7 +22,7 @@ namespace SeqCli.Cli.Commands.Forwarder; -[Command("forwarder", "start", "Start the forwarder Windows service", IsPreview = true)] +[Command("forwarder", "start", "Start the forwarder Windows service", Visibility = FeatureVisibility.Preview)] [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 f7733460..5339defa 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/StatusCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/StatusCommand.cs @@ -22,7 +22,7 @@ namespace SeqCli.Cli.Commands.Forwarder; -[Command("forwarder", "status", "Show the status of the forwarder Windows service", IsPreview = true)] +[Command("forwarder", "status", "Show the status of the forwarder Windows service", Visibility = FeatureVisibility.Preview)] [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 0dc72a45..b86695b4 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/StopCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/StopCommand.cs @@ -22,7 +22,7 @@ namespace SeqCli.Cli.Commands.Forwarder; -[Command("forwarder", "stop", "Stop the forwarder Windows service", IsPreview = true)] +[Command("forwarder", "stop", "Stop the forwarder Windows service", Visibility = FeatureVisibility.Preview)] [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 2ee8e01e..a62b7df9 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/TruncateCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/TruncateCommand.cs @@ -20,7 +20,7 @@ namespace SeqCli.Cli.Commands.Forwarder; -[Command("forwarder", "truncate", "Empty the forwarder's persistent log buffer", IsPreview = true)] +[Command("forwarder", "truncate", "Empty the forwarder's persistent log buffer", Visibility = FeatureVisibility.Preview)] 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 eb3299b1..80e9ffbc 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/UninstallCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/UninstallCommand.cs @@ -22,7 +22,7 @@ namespace SeqCli.Cli.Commands.Forwarder; -[Command("forwarder", "uninstall", "Uninstall the forwarder Windows service", IsPreview = true)] +[Command("forwarder", "uninstall", "Uninstall the forwarder Windows service", Visibility = FeatureVisibility.Preview)] class UninstallCommand : Command { protected override Task Run() diff --git a/src/SeqCli/Cli/Commands/HelpCommand.cs b/src/SeqCli/Cli/Commands/HelpCommand.cs index 3aab532b..b274e4bd 100644 --- a/src/SeqCli/Cli/Commands/HelpCommand.cs +++ b/src/SeqCli/Cli/Commands/HelpCommand.cs @@ -25,20 +25,25 @@ namespace SeqCli.Cli.Commands; [Command("help", "Show information about available commands", Example = "seqcli help search")] class HelpCommand : Command { - readonly IEnumerable, CommandMetadata>> _availableCommands; - bool _markdown, _pre; + readonly IEnumerable, CommandMetadata>> _allCommands; + bool _markdown; + FeatureVisibility _included = FeatureVisibility.Visible; - public HelpCommand(IEnumerable, CommandMetadata>> availableCommands) + public HelpCommand(IEnumerable, CommandMetadata>> allCommands) { - _availableCommands = availableCommands; - Options.Add("pre", "Show preview commands", _ => _pre = true); + _allCommands = allCommands.OrderBy(c => c.Metadata.Name).ToList(); + + Options.Add("pre", "Show preview commands", _ => _included |= FeatureVisibility.Preview); + Options.Add("hidden", "Show hidden commands", _ => _included |= FeatureVisibility.Hidden, true); Options.Add("m|markdown", "Generate markdown for use in documentation", _ => _markdown = true); } + IEnumerable, CommandMetadata>> AvailableCommands() => + _allCommands.Where(c => _included.HasFlag(c.Metadata.Visibility)); + protected override Task Run(string[] unrecognized) { - var orderedCommands = _availableCommands - .Where(c => !c.Metadata.IsPreview || _pre) + var orderedCommands = AvailableCommands() .OrderBy(c => c.Metadata.Name) .ThenBy(c => c.Metadata.SubCommand) .ToList(); diff --git a/src/SeqCli/Cli/FeatureVisibility.cs b/src/SeqCli/Cli/FeatureVisibility.cs new file mode 100644 index 00000000..80108738 --- /dev/null +++ b/src/SeqCli/Cli/FeatureVisibility.cs @@ -0,0 +1,26 @@ +// Copyright © Datalust Pty Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; + +namespace SeqCli.Cli; + +[Flags] +public enum FeatureVisibility +{ + None = 0, + Visible = 1, + Preview = 1 << 1, + Hidden = 1 << 2 +} diff --git a/src/SeqCli/Cli/Features/ConfigKeyFeature.cs b/src/SeqCli/Cli/Features/ConfigKeyFeature.cs new file mode 100644 index 00000000..e1efba2d --- /dev/null +++ b/src/SeqCli/Cli/Features/ConfigKeyFeature.cs @@ -0,0 +1,22 @@ +using System; +using SeqCli.Util; + +namespace SeqCli.Cli.Features; + +class ConfigKeyFeature: CommandFeature +{ + string? _key; + + public override void Enable(OptionSet options) + { + options.Add("k|key=", "The field, for example `connection.serverUrl`", k => _key = ArgumentString.Normalize(k)); + } + + public string GetKey() + { + if (string.IsNullOrEmpty(_key)) + throw new ArgumentException("A field must be supplied with `--key=KEY`."); + + return _key; + } +} diff --git a/src/SeqCli/Cli/Features/ConfigValueFeature.cs b/src/SeqCli/Cli/Features/ConfigValueFeature.cs new file mode 100644 index 00000000..45ce14ae --- /dev/null +++ b/src/SeqCli/Cli/Features/ConfigValueFeature.cs @@ -0,0 +1,38 @@ +using System; + +namespace SeqCli.Cli.Features; + +class ConfigValueFeature: CommandFeature +{ + // An empty string is normalized to null/unset, which will normally be considered "cleared"; we allow this + // to keep the CLI backwards-compatible. + bool _valueSpecified; + + string? Value { get; set; } + bool ReadValueFromStdin { get; set; } + + public override void Enable(OptionSet options) + { + options.Add("v|value=", + "The field value, comma-separated if multiple values are accepted", + v => + { + _valueSpecified = true; + // Not normalized; some settings might include leading/trailing whitespace. + Value = v; + }); + + options.Add("value-stdin", + "Read the value from `STDIN`", + _ => ReadValueFromStdin = true); + } + + public string? ReadValue() + { + if (!_valueSpecified && !ReadValueFromStdin) + throw new ArgumentException( + "A value must be supplied with either `--value=VALUE` or `--value-stdin`."); + + return ReadValueFromStdin ? Console.In.ReadToEnd().TrimEnd('\r', '\n') : Value; + } +} diff --git a/test/SeqCli.Tests/Cli/CommandLineHostTests.cs b/test/SeqCli.Tests/Cli/CommandLineHostTests.cs index b17ca6bb..73586db1 100644 --- a/test/SeqCli.Tests/Cli/CommandLineHostTests.cs +++ b/test/SeqCli.Tests/Cli/CommandLineHostTests.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Autofac.Features.Metadata; using SeqCli.Cli; -using SeqCli.Cli.Features; using SeqCli.Tests.Support; using Serilog.Core; using Serilog.Events; @@ -41,7 +40,7 @@ public async Task PrereleaseCommandsAreIgnoredWithoutFlag() { new( new Lazy(() => new ActionCommand(() => executed.Add("test"))), - new CommandMetadata {Name = "test", HelpText = "help", IsPreview = true}), + new CommandMetadata {Name = "test", HelpText = "help", Visibility = FeatureVisibility.Preview}), }; var commandLineHost = new CommandLineHost(availableCommands); var exit = await commandLineHost.Run(["test"],new LoggingLevelSwitch());