Skip to content

Commit 43b9787

Browse files
committed
Optimized enumerable/collection type parsing for CLI options/arguments
It's now fully compatible (stable) with AOT compilation ! Arrays, lists, collections - any type that implements `IEnumerable<T>` and has a public constructor with a `IEnumerable<T>` or `IList<T>` parameter (other parameters, if any, should be optional). If type is generic `IEnumerable<T>`, `IList<T>`, `ICollection<T>` interfaces itself, array `T[]` will be used. If type is non-generic `IEnumerable`, `IList`, `ICollection` interfaces itself, array `object[]` will be used.
1 parent 6158b34 commit 43b9787

File tree

210 files changed

+2348
-829
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

210 files changed

+2348
-829
lines changed

docs/README.md

Lines changed: 101 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public class RootCliCommand
4242
public string Option1 { get; set; } = "DefaultForOption1";
4343

4444
[CliArgument(Description = "Description for Argument1")]
45-
public string Argument1 { get; set; } = "DefaultForArgument1";
45+
public string Argument1 { get; set; }
4646

4747
public void Run()
4848
{
@@ -137,14 +137,43 @@ When you run the app via
137137
- `TestApp.exe` in project output path (e.g. in `TestApp\bin\Debug\net6.0`)
138138
- or `dotnet run`in project directory (e.g. in `TestApp`)
139139

140+
You see this result:
141+
```console
142+
Required argument missing for command: 'TestApp'.
143+
```
144+
This is because a `CliArgument` decorated property is required by default (`CliArgument.Required` property's default value is `true`).
145+
A `CliArgument` is a parameter for the command itself (for the root command - the exe in this case), that's why it's required by default.
146+
147+
If you want to make a `CliArgument` optional, set `CliArgument.Required` property to `false` and set a default value for the decorated property.
148+
In that case, the default value for the decorated property will be used when the user does not specify the argument on the command line.
149+
```c#
150+
[CliArgument(Required = false)]
151+
public string Argument1 { get; set; } = "DefaultForArgument1";
152+
```
153+
---
154+
When you run,
155+
```console
156+
TestApp.exe NewValueForArgument1
157+
```
158+
or (note the double hyphen/dash which allows `dotnet run` to pass arguments to our actual application):
159+
```console
160+
dotnet run -- NewValueForArgument1
161+
```
140162
You see this result:
141163
```console
142164
Handler for 'TestApp.Commands.RootCliCommand' is run:
143165
Value for Option1 property is 'DefaultForOption1'
144-
Value for Argument1 property is 'DefaultForArgument1'
166+
Value for Argument1 property is 'NewValueForArgument1'
145167
```
146-
As we set default values for properties in the class, the option and the argument were already populated (even when the user did not pass any values).
168+
This is because a `CliOption` decorated property is not required by default (`CliOption.Required` property's default value is `false`).
169+
A `CliOption` is optional, as the name implies, for the command itself (for the root command - the exe in this case), that's why it's not required by default.
147170

171+
If you want to make a `CliOption` required, set `CliArgument.Required` property to `true`.
172+
In that case, the default value for the decorated property will be ignored (if exists) and the user has to specify the option on the command line.
173+
```c#
174+
[CliOption(Required = true)]
175+
public string Option1 { get; set; }
176+
```
148177
---
149178
When you run,
150179
```console
@@ -179,7 +208,6 @@ The following types for properties is supported:
179208
But an option whose argument type is `bool` doesn't require an argument to be specified.
180209
The presence of the option token on the command line, with no argument following it, results in a value of `true`.
181210
* Enums - The values are bound by name, and the binding is case insensitive
182-
* Arrays and lists (any IEnumerable type)
183211
* Common CLR types:
184212

185213
* `string`, `bool`
@@ -191,7 +219,37 @@ The following types for properties is supported:
191219
* `Guid`
192220
* `Uri`, `IPAddress`, `IPEndPoint`
193221

194-
* Any type with a public constructor or a static `Parse` method with a string parameter - These types can be bound/parsed
222+
* Arrays, lists, collections - any type that implements `IEnumerable<T>` and has a public constructor with a `IEnumerable<T>` or `IList<T>` parameter (other parameters, if any, should be optional).
223+
If type is generic `IEnumerable<T>`, `IList<T>`, `ICollection<T>` interfaces itself, array `T[]` will be used.
224+
If type is non-generic `IEnumerable`, `IList`, `ICollection` interfaces itself, array `object[]` will be used.
225+
```c#
226+
[CliCommand]
227+
public class EnumerableCliCommand
228+
{
229+
[CliOption]
230+
public IEnumerable<int> OptEnumerable { get; set; }
231+
232+
[CliOption]
233+
public List<string> OptList { get; set; }
234+
235+
[CliOption(AllowMultipleArgumentsPerToken = true)]
236+
public FileAccess[] OptEnumArray { get; set; }
237+
238+
[CliOption]
239+
public Collection<string> OptCollection { get; set; }
240+
241+
[CliOption]
242+
public HashSet<string> OptHashSet { get; set; }
243+
244+
[CliOption]
245+
public Queue<FileInfo> OptQueue { get; set; }
246+
247+
[CliArgument]
248+
public IList ArgIList { get; set; }
249+
}
250+
```
251+
252+
* Any type with a public constructor or a static `Parse` method with a string parameter (other parameters, if any, should be optional) - These types can be bound/parsed
195253
automatically even if they are wrapped with `Enumerable` or `Nullable` type.
196254
```c#
197255
[CliCommand]
@@ -200,7 +258,7 @@ The following types for properties is supported:
200258
[CliOption]
201259
public ClassWithConstructor Opt { get; set; }
202260

203-
[CliOption]
261+
[CliOption(AllowMultipleArgumentsPerToken = true)]
204262
public ClassWithConstructor[] OptArray { get; set; }
205263

206264
[CliOption]
@@ -212,29 +270,11 @@ The following types for properties is supported:
212270
[CliOption]
213271
public List<ClassWithConstructor> OptList { get; set; }
214272

273+
[CliOption]
274+
public CustomList<ClassWithConstructor> OptCustomList { get; set; }
275+
215276
[CliArgument]
216277
public IEnumerable<Sub.ClassWithParser> Arg { get; set; }
217-
218-
public void Run()
219-
{
220-
Console.WriteLine($@"Handler for '{GetType().FullName}' is run:");
221-
222-
foreach (var property in GetType().GetProperties())
223-
{
224-
var value = property.GetValue(this);
225-
if (value is IEnumerable enumerable)
226-
value = string.Join(", ",
227-
enumerable
228-
.Cast<object>()
229-
.Select(s => s.ToString())
230-
);
231-
232-
Console.WriteLine($@"Value for {property.Name} property is '{value}'");
233-
234-
}
235-
236-
Console.WriteLine();
237-
}
238278
}
239279

240280
public class ClassWithConstructor
@@ -251,7 +291,7 @@ The following types for properties is supported:
251291
return value;
252292
}
253293
}
254-
294+
255295
public struct CustomStruct
256296
{
257297
private readonly string value;
@@ -271,40 +311,48 @@ The following types for properties is supported:
271311
{
272312
public class ClassWithParser
273313
{
274-
private readonly string value;
275-
276-
private ClassWithParser(string value)
277-
{
278-
this.value = value;
279-
}
314+
public string Value { get; set; }
280315

281316
public override string ToString()
282317
{
283-
return value;
318+
return Value;
284319
}
285320

286321
public static ClassWithParser Parse(string value)
287322
{
288-
return new ClassWithParser(value);
323+
var instance = new ClassWithParser
324+
{
325+
Value = value
326+
};
327+
return instance;
289328
}
290329
}
291330
}
331+
332+
public class CustomList<T> : List<T>
333+
{
334+
public CustomList(IEnumerable<T> items)
335+
: base(items)
336+
{
337+
338+
}
339+
}
292340
```
293341

294342
## Help output
295343

296344
When you run the app via `TestApp.exe -?` or `dotnet run -- -?`, you see this usage help:
297345
```console
298-
DotMake Command-Line TestApp v1.4.0
346+
DotMake Command-Line TestApp v1.5.4
299347
Copyright © 2023 DotMake
300348

301349
A root cli command
302350

303351
Usage:
304-
TestApp [<argument-1>] [options]
352+
TestApp <argument-1> [options]
305353

306354
Arguments:
307-
<argument-1> Description for Argument1 [default: DefaultForArgument1]
355+
<argument-1> Description for Argument1 [required]
308356

309357
Options:
310358
-o, --option-1 <option-1> Description for Option1 [default: DefaultForOption1]
@@ -351,13 +399,13 @@ using DotMake.CommandLine;
351399
NamePrefixConvention = CliNamePrefixConvention.ForwardSlash,
352400
ShortFormPrefixConvention = CliNamePrefixConvention.ForwardSlash
353401
)]
354-
public class RootCliCommand
402+
public class RootSnakeSlashCliCommand
355403
{
356404
[CliOption(Description = "Description for Option1")]
357405
public string Option1 { get; set; } = "DefaultForOption1";
358406

359407
[CliArgument(Description = "Description for Argument1")]
360-
public string Argument1 { get; set; } = "DefaultForArgument1";
408+
public string Argument1 { get; set; }
361409

362410
public void Run()
363411
{
@@ -370,14 +418,16 @@ public class RootCliCommand
370418
```
371419
When you run the app via `TestApp.exe -?` or `dotnet run -- -?`, you see this usage help:
372420
```console
373-
Description:
374-
A cli command with snake_case name casing and forward slash prefix conventions
421+
DotMake Command-Line TestApp v1.5.4
422+
Copyright © 2023 DotMake
423+
424+
A cli command with snake_case name casing and forward slash prefix conventions
375425

376426
Usage:
377-
TestApp [<argument_1>] [options]
427+
TestApp <argument_1> [options]
378428

379429
Arguments:
380-
<argument_1> Description for Argument1 [default: DefaultForArgument1]
430+
<argument_1> Description for Argument1 [required]
381431

382432
Options:
383433
/o, /option_1 <option_1> Description for Option1 [default: DefaultForOption1]
@@ -413,7 +463,7 @@ public class WithNestedChildrenCliCommand
413463
public string Option1 { get; set; } = "DefaultForOption1";
414464

415465
[CliArgument(Description = "Description for Argument1")]
416-
public string Argument1 { get; set; } = "DefaultForArgument1";
466+
public string Argument1 { get; set; }
417467

418468
public void Run()
419469
{
@@ -430,7 +480,7 @@ public class WithNestedChildrenCliCommand
430480
public string Option1 { get; set; } = "DefaultForOption1";
431481

432482
[CliArgument(Description = "Description for Argument1")]
433-
public string Argument1 { get; set; } = "DefaultForArgument1";
483+
public string Argument1 { get; set; }
434484

435485
public void Run()
436486
{
@@ -447,7 +497,7 @@ public class WithNestedChildrenCliCommand
447497
public string Option1 { get; set; } = "DefaultForOption1";
448498

449499
[CliArgument(Description = "Description for Argument1")]
450-
public string Argument1 { get; set; } = "DefaultForArgument1";
500+
public string Argument1 { get; set; }
451501

452502
public void Run()
453503
{
@@ -472,7 +522,7 @@ public class RootCliCommand
472522
public string Option1 { get; set; } = "DefaultForOption1";
473523

474524
[CliArgument(Description = "Description for Argument1")]
475-
public string Argument1 { get; set; } = "DefaultForArgument1";
525+
public string Argument1 { get; set; }
476526

477527
public void Run()
478528
{
@@ -494,7 +544,7 @@ public class ExternalLevel1SubCliCommand
494544
public string Option1 { get; set; } = "DefaultForOption1";
495545

496546
[CliArgument(Description = "Description for Argument1")]
497-
public string Argument1 { get; set; } = "DefaultForArgument1";
547+
public string Argument1 { get; set; }
498548

499549
public void Run()
500550
{
@@ -511,7 +561,7 @@ public class ExternalLevel1SubCliCommand
511561
public string Option1 { get; set; } = "DefaultForOption1";
512562

513563
[CliArgument(Description = "Description for Argument1")]
514-
public string Argument1 { get; set; } = "DefaultForArgument1";
564+
public string Argument1 { get; set; }
515565

516566
public void Run()
517567
{

0 commit comments

Comments
 (0)