Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Command line utils: allow 'inherited' options #131

Merged
merged 1 commit into from
Jun 29, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ public CommandLineApplication(bool throwOnUnexpectedArg = true)
public Func<string> ShortVersionGetter { get; set; }
public readonly List<CommandLineApplication> Commands;

public IEnumerable<CommandOption> 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<CommandLineApplication> configuration,
bool throwOnUnexpectedArg = true)
{
Expand All @@ -53,13 +66,21 @@ public CommandLineApplication Command(string name, Action<CommandLineApplication
}

public CommandOption Option(string template, string description, CommandOptionType optionType)
{
return Option(template, description, optionType, _ => { });
}
=> Option(template, description, optionType, _ => { }, inherited: false);

public CommandOption Option(string template, string description, CommandOptionType optionType, bool inherited)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style: add a newline above this method.

=> Option(template, description, optionType, _ => { }, inherited);

public CommandOption Option(string template, string description, CommandOptionType optionType, Action<CommandOption> configuration)
=> Option(template, description, optionType, configuration, inherited: false);

public CommandOption Option(string template, string description, CommandOptionType optionType, Action<CommandOption> 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;
Expand Down Expand Up @@ -95,7 +116,6 @@ public void OnExecute(Func<Task<int>> invoke)
{
Invoke = () => invoke().Result;
}

public int Execute(params string[] args)
{
CommandLineApplication command = this;
Expand All @@ -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)
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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));
}

Expand Down Expand Up @@ -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)
{
Expand All @@ -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();
Expand All @@ -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))
{
Expand All @@ -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()
Expand Down Expand Up @@ -435,36 +469,6 @@ public void ShowRootCommandFullNameAndVersion()
Console.WriteLine();
}

private int MaxOptionTemplateLength(IEnumerable<CommandOption> options)
{
var maxLen = 0;
foreach (var opt in options)
{
maxLen = opt.Template.Length > maxLen ? opt.Template.Length : maxLen;
}
return maxLen;
}

private int MaxCommandLength(IEnumerable<CommandLineApplication> commands)
{
var maxLen = 0;
foreach (var cmd in commands)
{
maxLen = cmd.Name.Length > maxLen ? cmd.Name.Length : maxLen;
}
return maxLen;
}

private int MaxArgumentLength(IEnumerable<CommandArgument> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ public CommandOption(string template, CommandOptionType optionType)
public List<string> Values { get; private set; }
public CommandOptionType OptionType { get; private set; }

public bool Inherited { get; set; }

public bool TryParse(string value)
{
switch (OptionType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -363,7 +363,7 @@ public void ThrowsExceptionOnUnexpectedOptionBeforeValidSubcommandByDefault()

app.Command("k", c =>
{
subCmd = c.Command("run", _=> { });
subCmd = c.Command("run", _ => { });
c.OnExecute(() => 0);
});

Expand All @@ -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<CommandParsingException>(() => app.Execute("subcmd", "-b", "B"));

Assert.Contains("-a|--option-a", subcmd.GetHelpText());
}

[Fact]
public void NestedOptionConflictThrows()
{
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you have nested options with the same name if they are not inherited (i.e. --ask would overwrite --always)? Can you add a test for this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, when options are not inherited (default), only the subcommand-local options are searched. I'll add a sanity test for it.

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<InvalidOperationException>(() => 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<CommandParsingException>(() => app.Execute("--nest2", "N2", "--nest1", "N1", "-g", "G"));
Assert.Throws<CommandParsingException>(() => 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);
}
}
}