Skip to content

Commit 8b08850

Browse files
committed
Make options, arguments, and commands read-only collections on CommandLineApplication (#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.
1 parent 7c573c1 commit 8b08850

13 files changed

+54
-29
lines changed

src/CommandLineUtils/Attributes/OptionAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ internal CommandOption Configure(CommandLineApplication app, PropertyInfo prop)
9090

9191
option.Description ??= prop.Name;
9292

93-
app.Options.Add(option);
93+
app.AddOption(option);
9494
return option;
9595
}
9696

src/CommandLineUtils/CommandLineApplication.cs

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ static CommandLineApplication()
5656
private readonly Lazy<IServiceProvider> _services;
5757
private readonly ConventionContext _conventionContext;
5858
private readonly List<IConvention> _conventions = new List<IConvention>();
59+
private readonly List<CommandArgument> _arguments = new List<CommandArgument>();
60+
private readonly List<CommandOption> _options = new List<CommandOption>();
61+
private readonly List<CommandLineApplication> _subcommands = new List<CommandLineApplication>();
62+
internal readonly List<string> _remainingArguments = new List<string>();
5963

6064
/// <summary>
6165
/// Initializes a new instance of <see cref="CommandLineApplication"/>.
@@ -109,10 +113,6 @@ internal CommandLineApplication(
109113
{
110114
_context = context ?? throw new ArgumentNullException(nameof(context));
111115
Parent = parent;
112-
Options = new List<CommandOption>();
113-
Arguments = new List<CommandArgument>();
114-
Commands = new List<CommandLineApplication>();
115-
RemainingArguments = new List<string>();
116116
_helpTextGenerator = helpTextGenerator ?? throw new ArgumentNullException(nameof(helpTextGenerator));
117117
_handler = DefaultAction;
118118
_validationErrorHandler = DefaultValidationErrorHandler;
@@ -186,7 +186,7 @@ public string? Name
186186
/// <summary>
187187
/// Available command-line options on this command. Use <see cref="GetOptions"/> to get all available options, which may include inherited options.
188188
/// </summary>
189-
public List<CommandOption> Options { get; private set; }
189+
public IReadOnlyCollection<CommandOption> Options => _options;
190190

191191
/// <summary>
192192
/// Whether a Pager should be used to display help text.
@@ -239,13 +239,13 @@ public CommandOption? OptionHelp
239239
/// <summary>
240240
/// Required command-line arguments.
241241
/// </summary>
242-
public List<CommandArgument> Arguments { get; private set; }
242+
public IReadOnlyList<CommandArgument> Arguments => _arguments;
243243

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

250250
/// <summary>
251251
/// Configures what the parser should do when it runs into an unexpected argument.
@@ -274,7 +274,7 @@ public UnrecognizedArgumentHandling UnrecognizedArgumentHandling
274274
/// <summary>
275275
/// Subcommands.
276276
/// </summary>
277-
public List<CommandLineApplication> Commands { get; private set; }
277+
public IReadOnlyCollection<CommandLineApplication> Commands => _subcommands;
278278

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

455455
subcommand.Parent = this;
456456

457-
Commands.Add(subcommand);
457+
_subcommands.Add(subcommand);
458458
}
459459

460460
private void AssertCommandNameIsUnique(string? name, CommandLineApplication? commandToIgnore)
@@ -562,11 +562,20 @@ public CommandOption Option(string template, string description, CommandOptionTy
562562
Description = description,
563563
Inherited = inherited
564564
};
565-
Options.Add(option);
565+
AddOption(option);
566566
configuration(option);
567567
return option;
568568
}
569569

570+
/// <summary>
571+
/// Add an option to the definition of this command
572+
/// </summary>
573+
/// <param name="option"></param>
574+
public void AddOption(CommandOption option)
575+
{
576+
_options.Add(option);
577+
}
578+
570579
/// <summary>
571580
/// Adds a command line option with values that should be parsable into <typeparamref name="T" />.
572581
/// </summary>
@@ -591,7 +600,7 @@ public CommandOption<T> Option<T>(string template, string description, CommandOp
591600
Description = description,
592601
Inherited = inherited
593602
};
594-
Options.Add(option);
603+
AddOption(option);
595604
configuration(option);
596605
return option;
597606
}
@@ -662,14 +671,18 @@ public CommandArgument<T> Argument<T>(string name, string description, Action<Co
662671
return argument;
663672
}
664673

665-
private void AddArgument(CommandArgument argument)
674+
/// <summary>
675+
/// Add an argument to the definition of this command.
676+
/// </summary>
677+
/// <param name="argument"></param>
678+
public void AddArgument(CommandArgument argument)
666679
{
667680
var lastArg = Arguments.LastOrDefault();
668681
if (lastArg != null && lastArg.MultipleValues)
669682
{
670683
throw new InvalidOperationException(Strings.OnlyLastArgumentCanAllowMultipleValues(lastArg.Name));
671684
}
672-
Arguments.Add(argument);
685+
_arguments.Add(argument);
673686
}
674687

675688
/// <summary>
@@ -708,7 +721,7 @@ private void Reset()
708721
}
709722

710723
IsShowingInformation = default;
711-
RemainingArguments.Clear();
724+
_remainingArguments.Clear();
712725
}
713726

714727
/// <summary>

src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public virtual void Apply(ConventionContext context)
6464
}
6565
}
6666

67-
context.Application.Arguments.Add(arg.Value);
67+
context.Application.AddArgument(arg.Value);
6868
}
6969
}
7070

src/CommandLineUtils/Conventions/DefaultHelpOptionConvention.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public void Apply(ConventionContext context)
7070
if (help.LongName != null || help.ShortName != null || help.SymbolName != null)
7171
{
7272
context.Application.OptionHelp = help;
73-
context.Application.Options.Add(help);
73+
context.Application.AddOption(help);
7474
}
7575
}
7676
}

src/CommandLineUtils/Conventions/RemainingArgsPropertyConvention.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Linq;
67
using System.Reflection;
78

89
namespace McMaster.Extensions.CommandLineUtils.Conventions

src/CommandLineUtils/Internal/CommandLineProcessor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ private bool ProcessUnexpectedArg(string argTypeName, string? argValue = null)
335335

336336
case UnrecognizedArgumentHandling.CollectAndContinue:
337337
var arg = _enumerator.Current;
338-
_currentCommand.RemainingArguments.Add(arg.Raw);
338+
_currentCommand._remainingArguments.Add(arg.Raw);
339339
return true;
340340

341341
case UnrecognizedArgumentHandling.StopParsingAndCollect:
@@ -354,7 +354,7 @@ private void AddRemainingArgumentValues()
354354
var arg = _enumerator.Current;
355355
if (arg != null)
356356
{
357-
_currentCommand.RemainingArguments.Add(arg.Raw);
357+
_currentCommand._remainingArguments.Add(arg.Raw);
358358
}
359359
} while (_enumerator.MoveNext());
360360
}

src/CommandLineUtils/PublicAPI.Shipped.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ McMaster.Extensions.CommandLineUtils.CommandLineApplication.AllowArgumentSeparat
105105
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Argument(string! name, string! description, bool multipleValues = false) -> McMaster.Extensions.CommandLineUtils.CommandArgument!
106106
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Argument(string! name, string! description, System.Action<McMaster.Extensions.CommandLineUtils.CommandArgument!>! configuration, bool multipleValues = false) -> McMaster.Extensions.CommandLineUtils.CommandArgument!
107107
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>!
108-
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Arguments.get -> System.Collections.Generic.List<McMaster.Extensions.CommandLineUtils.CommandArgument!>!
108+
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Arguments.get -> System.Collections.Generic.IReadOnlyList<McMaster.Extensions.CommandLineUtils.CommandArgument!>!
109109
McMaster.Extensions.CommandLineUtils.CommandLineApplication.ClusterOptions.get -> bool
110110
McMaster.Extensions.CommandLineUtils.CommandLineApplication.ClusterOptions.set -> void
111111
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Command(string! name, System.Action<McMaster.Extensions.CommandLineUtils.CommandLineApplication!>! configuration) -> McMaster.Extensions.CommandLineUtils.CommandLineApplication!
@@ -114,7 +114,7 @@ McMaster.Extensions.CommandLineUtils.CommandLineApplication.CommandLineApplicati
114114
McMaster.Extensions.CommandLineUtils.CommandLineApplication.CommandLineApplication(McMaster.Extensions.CommandLineUtils.HelpText.IHelpTextGenerator! helpTextGenerator, McMaster.Extensions.CommandLineUtils.IConsole! console, string! workingDirectory) -> void
115115
McMaster.Extensions.CommandLineUtils.CommandLineApplication.CommandLineApplication(McMaster.Extensions.CommandLineUtils.IConsole! console, string! workingDirectory) -> void
116116
McMaster.Extensions.CommandLineUtils.CommandLineApplication.CommandLineApplication(McMaster.Extensions.CommandLineUtils.IConsole! console) -> void
117-
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Commands.get -> System.Collections.Generic.List<McMaster.Extensions.CommandLineUtils.CommandLineApplication!>!
117+
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Commands.get -> System.Collections.Generic.IReadOnlyCollection<McMaster.Extensions.CommandLineUtils.CommandLineApplication!>!
118118
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Conventions.get -> McMaster.Extensions.CommandLineUtils.Conventions.IConventionBuilder!
119119
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Description.get -> string?
120120
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Description.set -> void
@@ -152,7 +152,7 @@ McMaster.Extensions.CommandLineUtils.CommandLineApplication.Option<T>(string! te
152152
McMaster.Extensions.CommandLineUtils.CommandLineApplication.OptionHelp.get -> McMaster.Extensions.CommandLineUtils.CommandOption?
153153
McMaster.Extensions.CommandLineUtils.CommandLineApplication.OptionNameValueSeparators.get -> char[]!
154154
McMaster.Extensions.CommandLineUtils.CommandLineApplication.OptionNameValueSeparators.set -> void
155-
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Options.get -> System.Collections.Generic.List<McMaster.Extensions.CommandLineUtils.CommandOption!>!
155+
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Options.get -> System.Collections.Generic.IReadOnlyCollection<McMaster.Extensions.CommandLineUtils.CommandOption!>!
156156
McMaster.Extensions.CommandLineUtils.CommandLineApplication.OptionsComparison.get -> System.StringComparison
157157
McMaster.Extensions.CommandLineUtils.CommandLineApplication.OptionsComparison.set -> void
158158
McMaster.Extensions.CommandLineUtils.CommandLineApplication.OptionVersion.get -> McMaster.Extensions.CommandLineUtils.CommandOption?
@@ -161,7 +161,7 @@ McMaster.Extensions.CommandLineUtils.CommandLineApplication.Out.set -> void
161161
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Parent.get -> McMaster.Extensions.CommandLineUtils.CommandLineApplication?
162162
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Parent.set -> void
163163
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Parse(params string![]! args) -> McMaster.Extensions.CommandLineUtils.Abstractions.ParseResult!
164-
McMaster.Extensions.CommandLineUtils.CommandLineApplication.RemainingArguments.get -> System.Collections.Generic.List<string!>!
164+
McMaster.Extensions.CommandLineUtils.CommandLineApplication.RemainingArguments.get -> System.Collections.Generic.IReadOnlyList<string!>!
165165
McMaster.Extensions.CommandLineUtils.CommandLineApplication.ResponseFileHandling.get -> McMaster.Extensions.CommandLineUtils.ResponseFileHandling
166166
McMaster.Extensions.CommandLineUtils.CommandLineApplication.ResponseFileHandling.set -> void
167167
McMaster.Extensions.CommandLineUtils.CommandLineApplication.ShortVersionGetter.get -> System.Func<string?>?

src/CommandLineUtils/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ McMaster.Extensions.CommandLineUtils.CommandArgument.HasValue.get -> bool
33
McMaster.Extensions.CommandLineUtils.CommandArgument.TryParse(string? value) -> bool
44
virtual McMaster.Extensions.CommandLineUtils.CommandArgument.Reset() -> void
55
virtual McMaster.Extensions.CommandLineUtils.CommandOption.Reset() -> void
6+
McMaster.Extensions.CommandLineUtils.CommandLineApplication.AddArgument(McMaster.Extensions.CommandLineUtils.CommandArgument! argument) -> void
7+
McMaster.Extensions.CommandLineUtils.CommandLineApplication.AddOption(McMaster.Extensions.CommandLineUtils.CommandOption! option) -> void

test/CommandLineUtils.Tests/CommandLineApplicationTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,7 @@ public void OptionNameCanHaveSemicolon()
745745
{
746746
LongName = "debug:hive"
747747
};
748-
app.Options.Add(option);
748+
app.AddOption(option);
749749
app.Parse("--debug:hive", "abc");
750750
Assert.Equal("abc", option.Value());
751751
}
@@ -759,7 +759,7 @@ public void OptionSeparatorMustNotUseSpace()
759759
{
760760
LongName = "debug:hive"
761761
};
762-
app.Options.Add(option);
762+
app.AddOption(option);
763763

764764
var ex = Assert.ThrowsAny<CommandParsingException>(() =>
765765
app.Parse("--debug:hive", "abc"));

test/CommandLineUtils.Tests/HelpOptionAttributeTests.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,10 @@ public void HelpOptionIsInherited()
169169
var outWriter = new StringWriter(sb);
170170
var app = new CommandLineApplication<Parent> { Out = outWriter };
171171
app.Conventions.UseDefaultConventions();
172-
app.Commands.ForEach(f => f.Out = outWriter);
172+
foreach (var subcmd in app.Commands)
173+
{
174+
subcmd.Out = outWriter;
175+
}
173176
app.Execute("lvl2", "--help");
174177
var outData = sb.ToString();
175178

@@ -210,7 +213,10 @@ public void NestedHelpOptionsChoosesHelpOptionNearestSelectedCommand(string[] ar
210213
return 1;
211214
});
212215

213-
app.Commands.ForEach(f => f.Out = outWriter);
216+
foreach (var subcmd in app.Commands)
217+
{
218+
subcmd.Out = outWriter;
219+
}
214220

215221
app.Execute(args);
216222
var outData = sb.ToString();

0 commit comments

Comments
 (0)