Skip to content

Commit

Permalink
Make options, arguments, and commands read-only collections on Comman…
Browse files Browse the repository at this point in the history
…dLineApplication (#407)

This gives CommandLineApplication more control over how the definition of the
application is constructed. Validation can be enforced, and refactoring of the internal
storage mechanism can happen without breaking the contract with callers.
  • Loading branch information
natemcmaster committed Nov 13, 2020
1 parent 6968682 commit 4ceb8db
Show file tree
Hide file tree
Showing 13 changed files with 54 additions and 29 deletions.
2 changes: 1 addition & 1 deletion src/CommandLineUtils/Attributes/OptionAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ internal CommandOption Configure(CommandLineApplication app, PropertyInfo prop)

option.Description ??= prop.Name;

app.Options.Add(option);
app.AddOption(option);
return option;
}

Expand Down
41 changes: 27 additions & 14 deletions src/CommandLineUtils/CommandLineApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ static CommandLineApplication()
private readonly Lazy<IServiceProvider> _services;
private readonly ConventionContext _conventionContext;
private readonly List<IConvention> _conventions = new List<IConvention>();
private readonly List<CommandArgument> _arguments = new List<CommandArgument>();
private readonly List<CommandOption> _options = new List<CommandOption>();
private readonly List<CommandLineApplication> _subcommands = new List<CommandLineApplication>();
internal readonly List<string> _remainingArguments = new List<string>();

/// <summary>
/// Initializes a new instance of <see cref="CommandLineApplication"/>.
Expand Down Expand Up @@ -109,10 +113,6 @@ internal CommandLineApplication(CommandLineApplication parent, string name)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
Parent = parent;
Options = new List<CommandOption>();
Arguments = new List<CommandArgument>();
Commands = new List<CommandLineApplication>();
RemainingArguments = new List<string>();
_helpTextGenerator = helpTextGenerator ?? throw new ArgumentNullException(nameof(helpTextGenerator));
_handler = DefaultAction;
_validationErrorHandler = DefaultValidationErrorHandler;
Expand Down Expand Up @@ -186,7 +186,7 @@ public IHelpTextGenerator HelpTextGenerator
/// <summary>
/// Available command-line options on this command. Use <see cref="GetOptions"/> to get all available options, which may include inherited options.
/// </summary>
public List<CommandOption> Options { get; private set; }
public IReadOnlyCollection<CommandOption> Options => _options;

/// <summary>
/// Whether a Pager should be used to display help text.
Expand Down Expand Up @@ -239,13 +239,13 @@ public IEnumerable<string> Names
/// <summary>
/// Required command-line arguments.
/// </summary>
public List<CommandArgument> Arguments { get; private set; }
public IReadOnlyList<CommandArgument> Arguments => _arguments;

/// <summary>
/// When initialized when <see cref="UnrecognizedArgumentHandling"/> is <see cref="McMaster.Extensions.CommandLineUtils.UnrecognizedArgumentHandling.StopParsingAndCollect" />,
/// this will contain any unrecognized arguments.
/// </summary>
public List<string> RemainingArguments { get; private set; }
public IReadOnlyList<string> RemainingArguments => _remainingArguments;

/// <summary>
/// Configures what the parser should do when it runs into an unexpected argument.
Expand Down Expand Up @@ -274,7 +274,7 @@ public UnrecognizedArgumentHandling UnrecognizedArgumentHandling
/// <summary>
/// Subcommands.
/// </summary>
public List<CommandLineApplication> Commands { get; private set; }
public IReadOnlyCollection<CommandLineApplication> Commands => _subcommands;

/// <summary>
/// Determines if '--' can be used to separate known arguments and options from additional content passed to <see cref="RemainingArguments"/>.
Expand Down Expand Up @@ -454,7 +454,7 @@ public void AddSubcommand(CommandLineApplication subcommand)

subcommand.Parent = this;

Commands.Add(subcommand);
_subcommands.Add(subcommand);
}

private void AssertCommandNameIsUnique(string? name, CommandLineApplication? commandToIgnore)
Expand Down Expand Up @@ -562,11 +562,20 @@ public CommandOption Option(string template, string description, CommandOptionTy
Description = description,
Inherited = inherited
};
Options.Add(option);
AddOption(option);
configuration(option);
return option;
}

/// <summary>
/// Add an option to the definition of this command
/// </summary>
/// <param name="option"></param>
public void AddOption(CommandOption option)
{
_options.Add(option);
}

/// <summary>
/// Adds a command line option with values that should be parsable into <typeparamref name="T" />.
/// </summary>
Expand All @@ -591,7 +600,7 @@ public CommandOption<T> Option<T>(string template, string description, CommandOp
Description = description,
Inherited = inherited
};
Options.Add(option);
AddOption(option);
configuration(option);
return option;
}
Expand Down Expand Up @@ -662,14 +671,18 @@ public CommandArgument<T> Argument<T>(string name, string description, Action<Co
return argument;
}

private void AddArgument(CommandArgument argument)
/// <summary>
/// Add an argument to the definition of this command.
/// </summary>
/// <param name="argument"></param>
public void AddArgument(CommandArgument argument)
{
var lastArg = Arguments.LastOrDefault();
if (lastArg != null && lastArg.MultipleValues)
{
throw new InvalidOperationException(Strings.OnlyLastArgumentCanAllowMultipleValues(lastArg.Name));
}
Arguments.Add(argument);
_arguments.Add(argument);
}

/// <summary>
Expand Down Expand Up @@ -708,7 +721,7 @@ private void Reset()
}

IsShowingInformation = default;
RemainingArguments.Clear();
_remainingArguments.Clear();
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public virtual void Apply(ConventionContext context)
}
}

context.Application.Arguments.Add(arg.Value);
context.Application.AddArgument(arg.Value);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public void Apply(ConventionContext context)
if (help.LongName != null || help.ShortName != null || help.SymbolName != null)
{
context.Application.OptionHelp = help;
context.Application.Options.Add(help);
context.Application.AddOption(help);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace McMaster.Extensions.CommandLineUtils.Conventions
Expand Down
4 changes: 2 additions & 2 deletions src/CommandLineUtils/Internal/CommandLineProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ private bool ProcessUnexpectedArg(string argTypeName, string? argValue = null)

case UnrecognizedArgumentHandling.CollectAndContinue:
var arg = _enumerator.Current;
_currentCommand.RemainingArguments.Add(arg.Raw);
_currentCommand._remainingArguments.Add(arg.Raw);
return true;

case UnrecognizedArgumentHandling.StopParsingAndCollect:
Expand All @@ -354,7 +354,7 @@ private void AddRemainingArgumentValues()
var arg = _enumerator.Current;
if (arg != null)
{
_currentCommand.RemainingArguments.Add(arg.Raw);
_currentCommand._remainingArguments.Add(arg.Raw);
}
} while (_enumerator.MoveNext());
}
Expand Down
8 changes: 4 additions & 4 deletions src/CommandLineUtils/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ McMaster.Extensions.CommandLineUtils.CommandLineApplication.AllowArgumentSeparat
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Argument(string! name, string! description, bool multipleValues = false) -> McMaster.Extensions.CommandLineUtils.CommandArgument!
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Argument(string! name, string! description, System.Action<McMaster.Extensions.CommandLineUtils.CommandArgument!>! configuration, bool multipleValues = false) -> McMaster.Extensions.CommandLineUtils.CommandArgument!
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Argument<T>(string! name, string! description, System.Action<McMaster.Extensions.CommandLineUtils.CommandArgument<T>!>! configuration, bool multipleValues = false) -> McMaster.Extensions.CommandLineUtils.CommandArgument<T>!
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Arguments.get -> System.Collections.Generic.List<McMaster.Extensions.CommandLineUtils.CommandArgument!>!
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Arguments.get -> System.Collections.Generic.IReadOnlyList<McMaster.Extensions.CommandLineUtils.CommandArgument!>!
McMaster.Extensions.CommandLineUtils.CommandLineApplication.ClusterOptions.get -> bool
McMaster.Extensions.CommandLineUtils.CommandLineApplication.ClusterOptions.set -> void
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Command(string! name, System.Action<McMaster.Extensions.CommandLineUtils.CommandLineApplication!>! configuration) -> McMaster.Extensions.CommandLineUtils.CommandLineApplication!
Expand All @@ -114,7 +114,7 @@ McMaster.Extensions.CommandLineUtils.CommandLineApplication.CommandLineApplicati
McMaster.Extensions.CommandLineUtils.CommandLineApplication.CommandLineApplication(McMaster.Extensions.CommandLineUtils.HelpText.IHelpTextGenerator! helpTextGenerator, McMaster.Extensions.CommandLineUtils.IConsole! console, string! workingDirectory) -> void
McMaster.Extensions.CommandLineUtils.CommandLineApplication.CommandLineApplication(McMaster.Extensions.CommandLineUtils.IConsole! console, string! workingDirectory) -> void
McMaster.Extensions.CommandLineUtils.CommandLineApplication.CommandLineApplication(McMaster.Extensions.CommandLineUtils.IConsole! console) -> void
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Commands.get -> System.Collections.Generic.List<McMaster.Extensions.CommandLineUtils.CommandLineApplication!>!
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Commands.get -> System.Collections.Generic.IReadOnlyCollection<McMaster.Extensions.CommandLineUtils.CommandLineApplication!>!
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Conventions.get -> McMaster.Extensions.CommandLineUtils.Conventions.IConventionBuilder!
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Description.get -> string?
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Description.set -> void
Expand Down Expand Up @@ -152,7 +152,7 @@ McMaster.Extensions.CommandLineUtils.CommandLineApplication.Option<T>(string! te
McMaster.Extensions.CommandLineUtils.CommandLineApplication.OptionHelp.get -> McMaster.Extensions.CommandLineUtils.CommandOption?
McMaster.Extensions.CommandLineUtils.CommandLineApplication.OptionNameValueSeparators.get -> char[]!
McMaster.Extensions.CommandLineUtils.CommandLineApplication.OptionNameValueSeparators.set -> void
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Options.get -> System.Collections.Generic.List<McMaster.Extensions.CommandLineUtils.CommandOption!>!
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Options.get -> System.Collections.Generic.IReadOnlyCollection<McMaster.Extensions.CommandLineUtils.CommandOption!>!
McMaster.Extensions.CommandLineUtils.CommandLineApplication.OptionsComparison.get -> System.StringComparison
McMaster.Extensions.CommandLineUtils.CommandLineApplication.OptionsComparison.set -> void
McMaster.Extensions.CommandLineUtils.CommandLineApplication.OptionVersion.get -> McMaster.Extensions.CommandLineUtils.CommandOption?
Expand All @@ -161,7 +161,7 @@ McMaster.Extensions.CommandLineUtils.CommandLineApplication.Out.set -> void
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Parent.get -> McMaster.Extensions.CommandLineUtils.CommandLineApplication?
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Parent.set -> void
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Parse(params string![]! args) -> McMaster.Extensions.CommandLineUtils.Abstractions.ParseResult!
McMaster.Extensions.CommandLineUtils.CommandLineApplication.RemainingArguments.get -> System.Collections.Generic.List<string!>!
McMaster.Extensions.CommandLineUtils.CommandLineApplication.RemainingArguments.get -> System.Collections.Generic.IReadOnlyList<string!>!
McMaster.Extensions.CommandLineUtils.CommandLineApplication.ResponseFileHandling.get -> McMaster.Extensions.CommandLineUtils.ResponseFileHandling
McMaster.Extensions.CommandLineUtils.CommandLineApplication.ResponseFileHandling.set -> void
McMaster.Extensions.CommandLineUtils.CommandLineApplication.ShortVersionGetter.get -> System.Func<string?>?
Expand Down
2 changes: 2 additions & 0 deletions src/CommandLineUtils/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ McMaster.Extensions.CommandLineUtils.CommandArgument.HasValue.get -> bool
McMaster.Extensions.CommandLineUtils.CommandArgument.TryParse(string? value) -> bool
virtual McMaster.Extensions.CommandLineUtils.CommandArgument.Reset() -> void
virtual McMaster.Extensions.CommandLineUtils.CommandOption.Reset() -> void
McMaster.Extensions.CommandLineUtils.CommandLineApplication.AddArgument(McMaster.Extensions.CommandLineUtils.CommandArgument! argument) -> void
McMaster.Extensions.CommandLineUtils.CommandLineApplication.AddOption(McMaster.Extensions.CommandLineUtils.CommandOption! option) -> void
4 changes: 2 additions & 2 deletions test/CommandLineUtils.Tests/CommandLineApplicationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,7 @@ public void OptionNameCanHaveSemicolon()
{
LongName = "debug:hive"
};
app.Options.Add(option);
app.AddOption(option);
app.Parse("--debug:hive", "abc");
Assert.Equal("abc", option.Value());
}
Expand All @@ -759,7 +759,7 @@ public void OptionSeparatorMustNotUseSpace()
{
LongName = "debug:hive"
};
app.Options.Add(option);
app.AddOption(option);

var ex = Assert.ThrowsAny<CommandParsingException>(() =>
app.Parse("--debug:hive", "abc"));
Expand Down
10 changes: 8 additions & 2 deletions test/CommandLineUtils.Tests/HelpOptionAttributeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,10 @@ public void HelpOptionIsInherited()
var outWriter = new StringWriter(sb);
var app = new CommandLineApplication<Parent> { Out = outWriter };
app.Conventions.UseDefaultConventions();
app.Commands.ForEach(f => f.Out = outWriter);
foreach (var subcmd in app.Commands)
{
subcmd.Out = outWriter;
}
app.Execute("lvl2", "--help");
var outData = sb.ToString();

Expand Down Expand Up @@ -210,7 +213,10 @@ public void NestedHelpOptionsChoosesHelpOptionNearestSelectedCommand(string[] ar
return 1;
});

app.Commands.ForEach(f => f.Out = outWriter);
foreach (var subcmd in app.Commands)
{
subcmd.Out = outWriter;
}

app.Execute(args);
var outData = sb.ToString();
Expand Down
3 changes: 2 additions & 1 deletion test/CommandLineUtils.Tests/OptionAttributeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using McMaster.Extensions.CommandLineUtils.Conventions;
Expand Down Expand Up @@ -314,7 +315,7 @@ private CommandOption CreateOption(Type propType, string propName)
var appBuilder = typeof(CommandLineApplication<>).MakeGenericType(program);
var app = (CommandLineApplication)Activator.CreateInstance(appBuilder, Util.EmptyArray<object>());
app.Conventions.UseOptionAttributes();
return app.Options[0];
return app.Options.First();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils.Abstractions;
using Xunit;
Expand Down Expand Up @@ -227,7 +228,7 @@ public void CustomParsersAreAvailableToSubCommands()

Assert.Equal(1, result);
Assert.Equal(expectedDate, app.Model.MainDate);
Assert.Equal(expectedDate.AddSeconds(123456), ((CommandLineApplication<CustomParserProgramAttributesSubCommand>)app.Commands[0]).Model.SubDate);
Assert.Equal(expectedDate.AddSeconds(123456), app.Commands.OfType<CommandLineApplication<CustomParserProgramAttributesSubCommand>>().Single().Model.SubDate);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils;
using McMaster.Extensions.CommandLineUtils.Abstractions;
Expand Down

0 comments on commit 4ceb8db

Please sign in to comment.