Skip to content

Commit 2b502c4

Browse files
authored
Support collecting all unrecognized arguments after parsing known args (#348)
1 parent 1769146 commit 2b502c4

File tree

4 files changed

+144
-75
lines changed

4 files changed

+144
-75
lines changed

src/CommandLineUtils/Internal/CommandLineProcessor.cs

Lines changed: 49 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -102,35 +102,13 @@ public ParseResult Process()
102102

103103
private bool ProcessNext()
104104
{
105-
switch (_enumerator.Current)
105+
return _enumerator.Current switch
106106
{
107-
case ArgumentSeparatorArgument _:
108-
if (!ProcessArgumentSeparator())
109-
{
110-
return false;
111-
}
112-
113-
break;
114-
case OptionArgument arg:
115-
if (!ProcessOption(arg))
116-
{
117-
return false;
118-
}
119-
120-
break;
121-
case CommandOrParameterArgument arg:
122-
if (!ProcessCommandOrParameter(arg))
123-
{
124-
return false;
125-
}
126-
127-
break;
128-
default:
129-
HandleUnexpectedArg("command or argument");
130-
return false;
131-
}
132-
133-
return true;
107+
ArgumentSeparatorArgument _ => ProcessArgumentSeparator(),
108+
OptionArgument arg => ProcessOption(arg),
109+
CommandOrParameterArgument arg => ProcessCommandOrParameter(arg),
110+
_ => ProcessUnexpectedArg("command or argument"),
111+
};
134112
}
135113

136114
private bool ProcessCommandOrParameter(CommandOrParameterArgument arg)
@@ -154,14 +132,10 @@ private bool ProcessCommandOrParameter(CommandOrParameterArgument arg)
154132
if (_currentCommandArguments.MoveNext())
155133
{
156134
_currentCommandArguments.Current.Values.Add(arg.Raw);
157-
}
158-
else
159-
{
160-
HandleUnexpectedArg("command or argument");
161-
return false;
135+
return true;
162136
}
163137

164-
return true;
138+
return ProcessUnexpectedArg("command or argument");
165139
}
166140

167141
private bool ProcessOption(OptionArgument arg)
@@ -173,6 +147,7 @@ private bool ProcessOption(OptionArgument arg)
173147
{
174148
if (_currentCommand.ClusterOptions)
175149
{
150+
var options = new List<CommandOption>();
176151
for (var i = 0; i < arg.Name.Length; i++)
177152
{
178153
var ch = arg.Name.Substring(i, 1);
@@ -187,56 +162,51 @@ private bool ProcessOption(OptionArgument arg)
187162

188163
if (option == null)
189164
{
190-
HandleUnexpectedArg("option", "-" + ch);
191-
return false;
165+
return ProcessUnexpectedArg("option", "-" + ch);
166+
}
167+
168+
options.Add(option);
169+
170+
if (option.OptionType != CommandOptionType.NoValue &&
171+
option.OptionType != CommandOptionType.SingleOrNoValue)
172+
{
173+
break;
192174
}
175+
}
176+
177+
for (var i = 0; i < options.Count - 1; i++)
178+
{
179+
option = options[i];
180+
option.TryParse(null);
193181

194182
// If we find a help/version option, show information and stop parsing
195183
if (_currentCommand.OptionHelp == option)
196184
{
197185
_currentCommand.ShowHelp();
198-
option.TryParse(null);
199186
return false;
200187
}
201188

202189
if (_currentCommand.OptionVersion == option)
203190
{
204191
_currentCommand.ShowVersion();
205-
option.TryParse(null);
206192
return false;
207193
}
194+
}
208195

209-
name = ch;
196+
option = options.Last();
197+
// supports specifying the value as the last bit of the flag. -Xignore-whitespace
198+
var trailingName = options.Count != arg.Name.Length
199+
? arg.Name.Substring(options.Count)
200+
: null;
210201

211-
var isLastChar = i == arg.Name.Length - 1;
212-
if (option.OptionType == CommandOptionType.NoValue)
213-
{
214-
if (!isLastChar)
215-
{
216-
option.TryParse(null);
217-
}
218-
}
219-
else if (option.OptionType == CommandOptionType.SingleOrNoValue)
220-
{
221-
if (!isLastChar)
222-
{
223-
option.TryParse(null);
224-
}
225-
}
226-
else if (!isLastChar)
227-
{
228-
if (value != null)
229-
{
230-
// if an option was also specified using :value or =value at the end of the option
231-
_currentCommand.ShowHint();
232-
throw new CommandParsingException(_currentCommand, $"Option '{ch}', which requires a value, must be the last option in a cluster");
233-
}
234-
235-
// supports specifying the value as the last bit of the flag. -Xignore-whitespace
236-
value = arg.Name.Substring(i + 1);
237-
break;
238-
}
202+
if (value != null && trailingName != null)
203+
{
204+
// if an option was also specified using :value or =value at the end of the option
205+
_currentCommand.ShowHint();
206+
throw new CommandParsingException(_currentCommand, $"Option '{option.ShortName}', which requires a value, must be the last option in a cluster");
239207
}
208+
209+
value ??= trailingName;
240210
}
241211
else
242212
{
@@ -255,8 +225,7 @@ private bool ProcessOption(OptionArgument arg)
255225

256226
if (option == null)
257227
{
258-
HandleUnexpectedArg("option");
259-
return false;
228+
return ProcessUnexpectedArg("option");
260229
}
261230

262231
// If we find a help/version option, show information and stop parsing
@@ -350,7 +319,7 @@ private bool ProcessArgumentSeparator()
350319
{
351320
if (!_currentCommand.AllowArgumentSeparator)
352321
{
353-
HandleUnexpectedArg("option");
322+
return ProcessUnexpectedArg("option");
354323
}
355324

356325
_enumerator.DisableResponseFileLoading = true;
@@ -363,7 +332,7 @@ private bool ProcessArgumentSeparator()
363332
return false;
364333
}
365334

366-
private void HandleUnexpectedArg(string argTypeName, string? argValue = null)
335+
private bool ProcessUnexpectedArg(string argTypeName, string? argValue = null)
367336
{
368337
switch (_config.UnrecognizedArgumentHandling)
369338
{
@@ -381,13 +350,18 @@ private void HandleUnexpectedArg(string argTypeName, string? argValue = null)
381350
throw new UnrecognizedCommandParsingException(_currentCommand, suggestions,
382351
$"Unrecognized {argTypeName} '{value}'");
383352

353+
case UnrecognizedArgumentHandling.CollectAndContinue:
354+
var arg = _enumerator.Current;
355+
_currentCommand.RemainingArguments.Add(arg.Raw);
356+
return true;
357+
384358
case UnrecognizedArgumentHandling.StopParsingAndCollect:
359+
// All remaining arguments are stored for further use
360+
AddRemainingArgumentValues();
361+
return false;
385362
default:
386-
break;
363+
throw new NotImplementedException();
387364
}
388-
389-
// All remaining arguments are stored for further use
390-
AddRemainingArgumentValues();
391365
}
392366

393367
private void AddRemainingArgumentValues()

src/CommandLineUtils/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ McMaster.Extensions.CommandLineUtils.CommandLineApplication.UnrecognizedArgument
1313
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Command(string name, System.Action<McMaster.Extensions.CommandLineUtils.CommandLineApplication> configuration) -> McMaster.Extensions.CommandLineUtils.CommandLineApplication
1414
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Command<TModel>(string name, System.Action<McMaster.Extensions.CommandLineUtils.CommandLineApplication<TModel>> configuration) -> McMaster.Extensions.CommandLineUtils.CommandLineApplication<TModel>
1515
McMaster.Extensions.CommandLineUtils.UnrecognizedArgumentHandling
16+
McMaster.Extensions.CommandLineUtils.UnrecognizedArgumentHandling.CollectAndContinue = 2 -> McMaster.Extensions.CommandLineUtils.UnrecognizedArgumentHandling
1617
McMaster.Extensions.CommandLineUtils.UnrecognizedArgumentHandling.StopParsingAndCollect = 1 -> McMaster.Extensions.CommandLineUtils.UnrecognizedArgumentHandling
1718
McMaster.Extensions.CommandLineUtils.UnrecognizedArgumentHandling.Throw = 0 -> McMaster.Extensions.CommandLineUtils.UnrecognizedArgumentHandling
1819
static McMaster.Extensions.CommandLineUtils.CommandLineApplicationExtensions.VersionOptionFromAssemblyAttributes(this McMaster.Extensions.CommandLineUtils.CommandLineApplication app, string template, System.Reflection.Assembly assembly) -> McMaster.Extensions.CommandLineUtils.CommandOption

src/CommandLineUtils/UnrecognizedArgumentHandling.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,11 @@ public enum UnrecognizedArgumentHandling
1818
/// including the first unrecognized argument, in <see cref="CommandLineApplication.RemainingArguments"/>.
1919
/// </summary>
2020
StopParsingAndCollect,
21+
22+
/// <summary>
23+
/// When an unrecognized argument is encountered, save it in a list that will be assigned
24+
/// to <see cref="CommandLineApplication.RemainingArguments"/>.
25+
/// </summary>
26+
CollectAndContinue,
2127
}
2228
}

test/CommandLineUtils.Tests/CommandLineApplicationTests.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,94 @@ public void AllowNoThrowBehaviorOnUnexpectedOptionAfterSubcommand()
494494
Assert.Equal(unexpectedOption, arg);
495495
}
496496

497+
[Fact]
498+
public void CollectUnrecognizedArguments()
499+
{
500+
var app = new CommandLineApplication
501+
{
502+
UnrecognizedArgumentHandling = UnrecognizedArgumentHandling.CollectAndContinue,
503+
};
504+
app.Argument("foo", "foo");
505+
506+
app.Execute("apple", "--foo", "bar", "banana");
507+
508+
Assert.Equal(new[] { "--foo", "bar", "banana" }, app.RemainingArguments.ToArray());
509+
}
510+
511+
[Fact]
512+
public void CollectUnrecognizedOptions()
513+
{
514+
var app = new CommandLineApplication
515+
{
516+
UnrecognizedArgumentHandling = UnrecognizedArgumentHandling.CollectAndContinue,
517+
};
518+
app.Option("--foo", "Foo", CommandOptionType.SingleValue);
519+
520+
app.Execute("apple", "--foo", "bar", "banana");
521+
522+
Assert.Equal(new[] { "apple", "banana" }, app.RemainingArguments.ToArray());
523+
}
524+
525+
[Theory]
526+
[InlineData("-Xabc")]
527+
[InlineData("-abcX")]
528+
[InlineData("-abXc")]
529+
public void CollectUnrecognizedClusteredOptions(string clusteredOptions)
530+
{
531+
var app = new CommandLineApplication
532+
{
533+
UnrecognizedArgumentHandling = UnrecognizedArgumentHandling.CollectAndContinue,
534+
};
535+
var optA = app.Option("-a", "Option a", CommandOptionType.NoValue);
536+
var optB = app.Option("-b", "Option b", CommandOptionType.NoValue);
537+
var optC = app.Option("-c", "Option c", CommandOptionType.NoValue);
538+
539+
app.Execute("apple", clusteredOptions, "banana");
540+
541+
Assert.Equal(new[] { "apple", clusteredOptions, "banana" }, app.RemainingArguments.ToArray());
542+
Assert.False(optA.HasValue());
543+
Assert.False(optB.HasValue());
544+
Assert.False(optC.HasValue());
545+
}
546+
547+
[Fact]
548+
public void CollectUnrecognizedOption()
549+
{
550+
var app = new CommandLineApplication
551+
{
552+
UnrecognizedArgumentHandling = UnrecognizedArgumentHandling.CollectAndContinue,
553+
};
554+
app.Execute("apple", "--foo", "bar", "banana");
555+
556+
Assert.Equal(new[] { "apple", "--foo", "bar", "banana" }, app.RemainingArguments.ToArray());
557+
}
558+
559+
[Fact]
560+
public void CollectUnrecognizedArgSeparator()
561+
{
562+
var app = new CommandLineApplication
563+
{
564+
UnrecognizedArgumentHandling = UnrecognizedArgumentHandling.CollectAndContinue,
565+
};
566+
app.Execute("apple", "--", "banana");
567+
568+
Assert.Equal(new[] { "apple", "--", "banana" }, app.RemainingArguments.ToArray());
569+
}
570+
571+
[Fact]
572+
public void CollectBeforeAndAfterArgSeparator()
573+
{
574+
var app = new CommandLineApplication
575+
{
576+
UnrecognizedArgumentHandling = UnrecognizedArgumentHandling.CollectAndContinue,
577+
AllowArgumentSeparator = true,
578+
};
579+
app.Option("--option", "abc", CommandOptionType.NoValue);
580+
app.Execute("apple", "--option", "--", "banana", "--option");
581+
582+
Assert.Equal(new[] { "apple", "banana", "--option" }, app.RemainingArguments.ToArray());
583+
}
584+
497585
[Fact]
498586
public void OptionsCanBeInherited()
499587
{

0 commit comments

Comments
 (0)