From 474dae7f496809d0bfe598aed4d13fd5c1a5f4fc Mon Sep 17 00:00:00 2001 From: qpooqp Date: Tue, 17 Nov 2020 21:14:12 +0100 Subject: [PATCH] Added functionality to specify default options for commands (#736) --- src/tye/Program.DefaultOptionsMiddleware.cs | 78 +++++++++++ src/tye/Program.cs | 3 + .../StandardOptions.cs | 14 +- .../DefaultOptionsMiddlewareTests.cs | 130 ++++++++++++++++++ 4 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 src/tye/Program.DefaultOptionsMiddleware.cs rename src/{Microsoft.Tye.Core => tye}/StandardOptions.cs (95%) create mode 100644 test/UnitTests/DefaultOptionsMiddlewareTests.cs diff --git a/src/tye/Program.DefaultOptionsMiddleware.cs b/src/tye/Program.DefaultOptionsMiddleware.cs new file mode 100644 index 00000000..dd2f743e --- /dev/null +++ b/src/tye/Program.DefaultOptionsMiddleware.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.CommandLine.Parsing; +using System.Linq; + +namespace Microsoft.Tye +{ + public static partial class Program + { + private const string DefaultOptionsEnvVarPrefix = "TYE_"; + private const string DefaultOptionsEnvVarPostfix = "_ARGS"; + + public static void DefaultOptionsMiddleware(InvocationContext context) + { + // Check if current command is child of root command - default options for deeper nested commands is not supported at the moment + if (context.ParseResult.CommandResult.Parent != context.ParseResult.RootCommandResult) + { + return; + } + + var commandName = context.ParseResult.CommandResult.Command.Name; + + // Get default options from environment variable for current command + var rawDefaultOptions = Environment.GetEnvironmentVariable(DefaultOptionsEnvVarPrefix + commandName.ToUpper() + DefaultOptionsEnvVarPostfix); + if (string.IsNullOrWhiteSpace(rawDefaultOptions)) + { + return; + } + + var originalParseResult = context.ParseResult; + // Get currently applied options + var originalOptionResults = GetCommandOptions(originalParseResult.CommandResult); + // Recreate orignial input + var originalTokens = StringifyTokens(originalParseResult.Tokens); + + // Exit early if --no-default option is applied + if (originalOptionResults.Any(option => option.Option.Name == StandardOptions.NoDefaultOptions.Name)) + { + return; + } + + // Parse default options to validate them + var defaultParseResult = context.Parser.Parse($"{commandName} {rawDefaultOptions}"); + // Get valid default options + var defaultOptionResults = GetCommandOptions(defaultParseResult.CommandResult); + // Get only options that are not already applied + var additionalTokens = GetAdditionalOptionsTokens(originalOptionResults, defaultOptionResults); + + // Set parse result as combination of original input plus default options + context.ParseResult = context.Parser.Parse($"{originalTokens} {additionalTokens}"); + + static string StringifyTokens(IEnumerable tokens) + { + return string.Join(" ", tokens.Select(t => t.Value)); + } + + static IEnumerable GetCommandOptions(CommandResult commandResult) + { + return commandResult.Children.OfType(); + } + + // Filter only options which are not already applied in original command or which are implicit + static string GetAdditionalOptionsTokens(IEnumerable originalOptions, IEnumerable defaultOptions) + { + var additionalOptions = defaultOptions + .Where(@default => !originalOptions.Any(original => !original.IsImplicit && original.Option.Name == @default.Option.Name)) + .Select(additional => $"{additional.Token.Value} {StringifyTokens(additional.Tokens)}"); + return string.Join(" ", additionalOptions); + } + } + } +} diff --git a/src/tye/Program.cs b/src/tye/Program.cs index eabd8abb..8a2ca608 100644 --- a/src/tye/Program.cs +++ b/src/tye/Program.cs @@ -23,6 +23,7 @@ namespace Microsoft.Tye { Description = "Developer tools and publishing for microservices.", }; + command.AddGlobalOption(StandardOptions.NoDefaultOptions); command.AddCommand(CreateInitCommand()); command.AddCommand(CreateGenerateCommand()); @@ -49,6 +50,8 @@ namespace Microsoft.Tye builder.CancelOnProcessTermination(); builder.UseExceptionHandler(HandleException); + builder.UseMiddleware(DefaultOptionsMiddleware); + var parser = builder.Build(); return parser.InvokeAsync(args); } diff --git a/src/Microsoft.Tye.Core/StandardOptions.cs b/src/tye/StandardOptions.cs similarity index 95% rename from src/Microsoft.Tye.Core/StandardOptions.cs rename to src/tye/StandardOptions.cs index fbed4d93..9747d05a 100644 --- a/src/Microsoft.Tye.Core/StandardOptions.cs +++ b/src/tye/StandardOptions.cs @@ -111,7 +111,6 @@ namespace Microsoft.Tye { get { - var argument = new Argument(TryParse, isDefault: true) { Arity = ArgumentArity.ZeroOrOne, @@ -227,6 +226,19 @@ namespace Microsoft.Tye } } + public static Option NoDefaultOptions + { + get + { + return new Option(new[] { "--no-default" }) + { + Description = "Disable default options from environment variables", + Required = false, + Argument = new Argument(), + }; + } + } + public static Option CreateForce(string descriptions) => new Option(new[] { "--force" }) { diff --git a/test/UnitTests/DefaultOptionsMiddlewareTests.cs b/test/UnitTests/DefaultOptionsMiddlewareTests.cs new file mode 100644 index 00000000..f8fa0718 --- /dev/null +++ b/test/UnitTests/DefaultOptionsMiddlewareTests.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Builder; +using System.CommandLine.Invocation; +using System.CommandLine.IO; +using System.CommandLine.Parsing; +using System.Linq; +using Xunit; + +namespace Microsoft.Tye.UnitTests +{ + public class DefaultOptionsMiddlewareTests + { + private readonly TestConsole _console = new TestConsole(); + private readonly Parser _parser; + + public DefaultOptionsMiddlewareTests() + { + var command = new Command("xxx") + { + Handler = CommandHandler.Create((IConsole console, ParseResult result) => + { + foreach (var option in result.CommandResult.Children.OfType()) + { + console.Out.Write(option.Token.Value); + var argument = option.Children.OfType().FirstOrDefault(); + if (argument?.Tokens.Count > 0) + { + console.Out.Write($":{argument.Tokens[0].Value}"); + } + console.Out.Write(" "); + } + }) + }; + + var subcommand = new Command("yyy"); + command.AddCommand(subcommand); + + var originalOption = new Option("--original"); + var defaultOption = new Option("--default"); + var implicitOption = new Option("--implicit") + { + Argument = new Argument(() => false) + { + Arity = ArgumentArity.ExactlyOne, + }, + }; + + var rootCommand = new RootCommand(); + rootCommand.AddGlobalOption(originalOption); + rootCommand.AddGlobalOption(defaultOption); + rootCommand.AddGlobalOption(implicitOption); + rootCommand.AddGlobalOption(StandardOptions.NoDefaultOptions); + rootCommand.AddCommand(command); + + _parser = new CommandLineBuilder(rootCommand) + .UseMiddleware(Program.DefaultOptionsMiddleware) + .Build(); + } + + private string[] OptionsFromConsole => _console.Out.ToString()?.Split(" ", StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty(); + + [Fact] + public void Should_apply_default_option() + { + Environment.SetEnvironmentVariable("TYE_XXX_ARGS", "--default", EnvironmentVariableTarget.Process); + + _parser.Invoke("xxx --original", _console); + + var appliedOptions = OptionsFromConsole; + Assert.Contains("--default", appliedOptions); + } + + [Fact] + public void Should_not_apply_default_option_if_command_is_root_command() + { + Environment.SetEnvironmentVariable("TYE_XXX_ARGS", "--default", EnvironmentVariableTarget.Process); + + _parser.Invoke("--original", _console); + + var appliedOptions = OptionsFromConsole; + Assert.DoesNotContain("--default", appliedOptions); + } + + [Fact] + public void Should_not_apply_default_option_if_command_is_not_child_of_root_command() + { + Environment.SetEnvironmentVariable("TYE_XXX_ARGS", "--default", EnvironmentVariableTarget.Process); + + _parser.Invoke("xxx yyy --original", _console); + + var appliedOptions = OptionsFromConsole; + Assert.DoesNotContain("--default", appliedOptions); + } + + [Fact] + public void Should_not_apply_default_option_if_env_var_is_empty() + { + Environment.SetEnvironmentVariable("TYE_XXX_ARGS", "", EnvironmentVariableTarget.Process); + + _parser.Invoke("xxx --original", _console); + + var appliedOptions = OptionsFromConsole; + Assert.DoesNotContain("--default", appliedOptions); + } + + [Fact] + public void Should_not_apply_default_option_if_it_is_already_applied() + { + Environment.SetEnvironmentVariable("TYE_XXX_ARGS", "--default", EnvironmentVariableTarget.Process); + + _parser.Invoke("xxx --original --default", _console); + + var appliedOptions = OptionsFromConsole; + Assert.Equal(1, appliedOptions.Count(o => o == "--default")); + } + + [Fact] + public void Should_apply_default_option_if_it_is_already_implicitly_applied() + { + Environment.SetEnvironmentVariable("TYE_XXX_ARGS", "--default --implicit true", EnvironmentVariableTarget.Process); + + _parser.Invoke("xxx --original", _console); + + var appliedOptions = OptionsFromConsole; + Assert.Contains("--implicit:true", appliedOptions); + } + } +}