Skip to content

Commit

Permalink
Command line: allow 'inherited' options
Browse files Browse the repository at this point in the history
  • Loading branch information
Nate McMaster committed Jun 29, 2016
1 parent 9e9c417 commit 1380d8d
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 47 deletions.
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(bool throwOnUnexpectedArg = true)
}

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)
=> 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()
{
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);
}
}
}

0 comments on commit 1380d8d

Please sign in to comment.