diff --git a/src/Microsoft.Extensions.CommandLineUtils/CommandLine/CommandLineApplication.cs b/src/Microsoft.Extensions.CommandLineUtils/CommandLine/CommandLineApplication.cs index edb3739d032..9ae085e7cfb 100644 --- a/src/Microsoft.Extensions.CommandLineUtils/CommandLine/CommandLineApplication.cs +++ b/src/Microsoft.Extensions.CommandLineUtils/CommandLine/CommandLineApplication.cs @@ -43,6 +43,19 @@ public CommandLineApplication(bool throwOnUnexpectedArg = true) public Func ShortVersionGetter { get; set; } public readonly List Commands; + public IEnumerable GetOptions() + { + var expr = Options.AsEnumerable(); + var rootNode = this; + while (rootNode.Parent != null) + { + rootNode = rootNode.Parent; + expr = expr.Concat(rootNode.Options.Where(o => o.Inherited)); + } + + return expr; + } + public CommandLineApplication Command(string name, Action configuration, bool throwOnUnexpectedArg = true) { @@ -53,13 +66,21 @@ public CommandLineApplication Command(string name, Action { }); - } + => Option(template, description, optionType, _ => { }, inherited: false); + + public CommandOption Option(string template, string description, CommandOptionType optionType, bool inherited) + => Option(template, description, optionType, _ => { }, inherited); public CommandOption Option(string template, string description, CommandOptionType optionType, Action configuration) + => Option(template, description, optionType, configuration, inherited: false); + + public CommandOption Option(string template, string description, CommandOptionType optionType, Action configuration, bool inherited) { - var option = new CommandOption(template, optionType) { Description = description }; + var option = new CommandOption(template, optionType) + { + Description = description, + Inherited = inherited + }; Options.Add(option); configuration(option); return option; @@ -95,7 +116,6 @@ public void OnExecute(Func> invoke) { Invoke = () => invoke().Result; } - public int Execute(params string[] args) { CommandLineApplication command = this; @@ -122,7 +142,7 @@ public int Execute(params string[] args) if (longOption != null) { processed = true; - option = command.Options.SingleOrDefault(opt => string.Equals(opt.LongName, longOption[0], StringComparison.Ordinal)); + option = command.GetOptions().SingleOrDefault(opt => string.Equals(opt.LongName, longOption[0], StringComparison.Ordinal)); if (option == null) { @@ -161,12 +181,12 @@ public int Execute(params string[] args) if (shortOption != null) { processed = true; - option = command.Options.SingleOrDefault(opt => string.Equals(opt.ShortName, shortOption[0], StringComparison.Ordinal)); + option = command.GetOptions().SingleOrDefault(opt => string.Equals(opt.ShortName, shortOption[0], StringComparison.Ordinal)); // If not a short option, try symbol option if (option == null) { - option = command.Options.SingleOrDefault(opt => string.Equals(opt.SymbolName, shortOption[0], StringComparison.Ordinal)); + option = command.GetOptions().SingleOrDefault(opt => string.Equals(opt.SymbolName, shortOption[0], StringComparison.Ordinal)); } if (option == null) @@ -313,10 +333,19 @@ public void ShowHint() // Show full help public void ShowHelp(string commandName = null) { - var headerBuilder = new StringBuilder("Usage:"); for (var cmd = this; cmd != null; cmd = cmd.Parent) { cmd.IsShowingInformation = true; + } + + Console.WriteLine(GetHelpText(commandName)); + } + + public string GetHelpText(string commandName = null) + { + var headerBuilder = new StringBuilder("Usage:"); + for (var cmd = this; cmd != null; cmd = cmd.Parent) + { headerBuilder.Insert(6, string.Format(" {0}", cmd.Name)); } @@ -352,7 +381,7 @@ public void ShowHelp(string commandName = null) argumentsBuilder.AppendLine(); argumentsBuilder.AppendLine("Arguments:"); - var maxArgLen = MaxArgumentLength(target.Arguments); + var maxArgLen = target.Arguments.Max(a => a.Name.Length); var outputFormat = string.Format(" {{0, -{0}}}{{1}}", maxArgLen + 2); foreach (var arg in target.Arguments) { @@ -361,15 +390,16 @@ public void ShowHelp(string commandName = null) } } - if (target.Options.Any()) + var options = target.GetOptions().ToList(); + if (options.Any()) { headerBuilder.Append(" [options]"); optionsBuilder.AppendLine(); optionsBuilder.AppendLine("Options:"); - var maxOptLen = MaxOptionTemplateLength(target.Options); + var maxOptLen = options.Max(o => o.Template.Length); var outputFormat = string.Format(" {{0, -{0}}}{{1}}", maxOptLen + 2); - foreach (var opt in target.Options) + foreach (var opt in options) { optionsBuilder.AppendFormat(outputFormat, opt.Template, opt.Description); optionsBuilder.AppendLine(); @@ -382,7 +412,7 @@ public void ShowHelp(string commandName = null) commandsBuilder.AppendLine(); commandsBuilder.AppendLine("Commands:"); - var maxCmdLen = MaxCommandLength(target.Commands); + var maxCmdLen = target.Commands.Max(c => c.Name.Length); var outputFormat = string.Format(" {{0, -{0}}}{{1}}", maxCmdLen + 2); foreach (var cmd in target.Commands.OrderBy(c => c.Name)) { @@ -404,7 +434,11 @@ public void ShowHelp(string commandName = null) nameAndVersion.AppendLine(GetFullNameAndVersion()); nameAndVersion.AppendLine(); - Console.Write("{0}{1}{2}{3}{4}", nameAndVersion, headerBuilder, argumentsBuilder, optionsBuilder, commandsBuilder); + return nameAndVersion.ToString() + + headerBuilder.ToString() + + argumentsBuilder.ToString() + + optionsBuilder.ToString() + + commandsBuilder.ToString(); } public void ShowVersion() @@ -435,36 +469,6 @@ public void ShowRootCommandFullNameAndVersion() Console.WriteLine(); } - private int MaxOptionTemplateLength(IEnumerable options) - { - var maxLen = 0; - foreach (var opt in options) - { - maxLen = opt.Template.Length > maxLen ? opt.Template.Length : maxLen; - } - return maxLen; - } - - private int MaxCommandLength(IEnumerable commands) - { - var maxLen = 0; - foreach (var cmd in commands) - { - maxLen = cmd.Name.Length > maxLen ? cmd.Name.Length : maxLen; - } - return maxLen; - } - - private int MaxArgumentLength(IEnumerable arguments) - { - var maxLen = 0; - foreach (var arg in arguments) - { - maxLen = arg.Name.Length > maxLen ? arg.Name.Length : maxLen; - } - return maxLen; - } - private void HandleUnexpectedArg(CommandLineApplication command, string[] args, int index, string argTypeName) { if (command._throwOnUnexpectedArg) diff --git a/src/Microsoft.Extensions.CommandLineUtils/CommandLine/CommandOption.cs b/src/Microsoft.Extensions.CommandLineUtils/CommandLine/CommandOption.cs index 7ed0dea75b6..71f25d485d4 100644 --- a/src/Microsoft.Extensions.CommandLineUtils/CommandLine/CommandOption.cs +++ b/src/Microsoft.Extensions.CommandLineUtils/CommandLine/CommandOption.cs @@ -60,6 +60,8 @@ public CommandOption(string template, CommandOptionType optionType) public List Values { get; private set; } public CommandOptionType OptionType { get; private set; } + public bool Inherited { get; set; } + public bool TryParse(string value) { switch (OptionType) diff --git a/test/Microsoft.Extensions.CommandLineUtils.Tests/CommandLineApplicationTests.cs b/test/Microsoft.Extensions.CommandLineUtils.Tests/CommandLineApplicationTests.cs index 046cb7efd10..906c9895822 100644 --- a/test/Microsoft.Extensions.CommandLineUtils.Tests/CommandLineApplicationTests.cs +++ b/test/Microsoft.Extensions.CommandLineUtils.Tests/CommandLineApplicationTests.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Threading.Tasks; +using System.Linq; using Microsoft.Extensions.CommandLineUtils; using Xunit; @@ -363,7 +363,7 @@ public void ThrowsExceptionOnUnexpectedOptionBeforeValidSubcommandByDefault() app.Command("k", c => { - subCmd = c.Command("run", _=> { }); + subCmd = c.Command("run", _ => { }); c.OnExecute(() => 0); }); @@ -390,5 +390,116 @@ public void AllowNoThrowBehaviorOnUnexpectedOptionAfterSubcommand() Assert.Equal(1, subCmd.RemainingArguments.Count); Assert.Equal(unexpectedOption, subCmd.RemainingArguments[0]); } + + [Fact] + public void OptionsCanBeInherited() + { + var app = new CommandLineApplication(); + var optionA = app.Option("-a|--option-a", "", CommandOptionType.SingleValue, inherited: true); + string optionAValue = null; + + var optionB = app.Option("-b", "", CommandOptionType.SingleValue, inherited: false); + + var subcmd = app.Command("subcmd", c => + { + c.OnExecute(() => + { + optionAValue = optionA.Value(); + return 0; + }); + }); + + Assert.Equal(2, app.GetOptions().Count()); + Assert.Equal(1, subcmd.GetOptions().Count()); + + app.Execute("-a", "A1", "subcmd"); + Assert.Equal("A1", optionAValue); + + Assert.Throws(() => app.Execute("subcmd", "-b", "B")); + + Assert.Contains("-a|--option-a", subcmd.GetHelpText()); + } + + [Fact] + public void NestedOptionConflictThrows() + { + var app = new CommandLineApplication(); + app.Option("-a|--always", "Top-level", CommandOptionType.SingleValue, inherited: true); + app.Command("subcmd", c => + { + c.Option("-a|--ask", "Nested", CommandOptionType.SingleValue); + }); + + Assert.Throws(() => app.Execute("subcmd", "-a", "b")); + } + + [Fact] + public void OptionsWithSameName() + { + var app = new CommandLineApplication(); + var top = app.Option("-a|--always", "Top-level", CommandOptionType.SingleValue, inherited: false); + CommandOption nested = null; + app.Command("subcmd", c => + { + nested = c.Option("-a|--ask", "Nested", CommandOptionType.SingleValue); + }); + + app.Execute("-a", "top"); + Assert.Equal("top", top.Value()); + Assert.Null(nested.Value()); + + top.Values.Clear(); + + app.Execute("subcmd", "-a", "nested"); + Assert.Null(top.Value()); + Assert.Equal("nested", nested.Value()); + } + + + [Fact] + public void NestedInheritedOptions() + { + string globalOptionValue = null, nest1OptionValue = null, nest2OptionValue = null; + + var app = new CommandLineApplication(); + CommandLineApplication subcmd2 = null; + var g = app.Option("-g|--global", "Global option", CommandOptionType.SingleValue, inherited: true); + var subcmd1 = app.Command("lvl1", s1 => + { + var n1 = s1.Option("--nest1", "Nested one level down", CommandOptionType.SingleValue, inherited: true); + subcmd2 = s1.Command("lvl2", s2 => + { + var n2 = s2.Option("--nest2", "Nested one level down", CommandOptionType.SingleValue, inherited: true); + s2.HelpOption("-h|--help"); + s2.OnExecute(() => + { + globalOptionValue = g.Value(); + nest1OptionValue = n1.Value(); + nest2OptionValue = n2.Value(); + return 0; + }); + }); + }); + + Assert.False(app.GetOptions().Any(o => o.LongName == "nest2")); + Assert.False(app.GetOptions().Any(o => o.LongName == "nest1")); + Assert.Contains(app.GetOptions(), o => o.LongName == "global"); + + Assert.False(subcmd1.GetOptions().Any(o => o.LongName == "nest2")); + Assert.Contains(subcmd1.GetOptions(), o => o.LongName == "nest1"); + Assert.Contains(subcmd1.GetOptions(), o => o.LongName == "global"); + + Assert.Contains(subcmd2.GetOptions(), o => o.LongName == "nest2"); + Assert.Contains(subcmd2.GetOptions(), o => o.LongName == "nest1"); + Assert.Contains(subcmd2.GetOptions(), o => o.LongName == "global"); + + Assert.Throws(() => app.Execute("--nest2", "N2", "--nest1", "N1", "-g", "G")); + Assert.Throws(() => app.Execute("lvl1", "--nest2", "N2", "--nest1", "N1", "-g", "G")); + + app.Execute("lvl1", "lvl2", "--nest2", "N2", "-g", "G", "--nest1", "N1"); + Assert.Equal("G", globalOptionValue); + Assert.Equal("N1", nest1OptionValue); + Assert.Equal("N2", nest2OptionValue); + } } }