You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In `ConsoleAppFramework`, the number and types of registered commands are statically determined at compile time. For example, let's register the following four commands:
336
+
337
+
```csharp
338
+
app.Add("foo", () => { });
339
+
app.Add("foo bar", (intx, inty) => { });
340
+
app.Add("foo bar barbaz", (DateTimedateTime) => { });
This ensures the fastest execution speed without any additional unnecessary allocations such as arrays and without any boxing since it holds static delegate types.
378
+
379
+
Command routing also generates a switch of nested string constants.
380
+
381
+
```csharp
382
+
partialvoidRunCore(string[] args)
383
+
{
384
+
if (args.Length==0)
385
+
{
386
+
ShowHelp(-1);
387
+
return;
388
+
}
389
+
switch (args[0])
390
+
{
391
+
case"foo":
392
+
if (args.Length==1)
393
+
{
394
+
RunCommand0(args, args.AsSpan(1), command0);
395
+
return;
396
+
}
397
+
switch (args[1])
398
+
{
399
+
case"bar":
400
+
if (args.Length==2)
401
+
{
402
+
RunCommand1(args, args.AsSpan(2), command1);
403
+
return;
404
+
}
405
+
switch (args[2])
406
+
{
407
+
case"barbaz":
408
+
RunCommand2(args, args.AsSpan(3), command2);
409
+
break;
410
+
default:
411
+
RunCommand1(args, args.AsSpan(2), command1);
412
+
break;
413
+
}
414
+
break;
415
+
case"baz":
416
+
RunCommand3(args, args.AsSpan(2), command3);
417
+
break;
418
+
default:
419
+
RunCommand0(args, args.AsSpan(1), command0);
420
+
break;
421
+
}
422
+
break;
423
+
default:
424
+
ShowHelp(-1);
425
+
break;
426
+
}
427
+
}
428
+
```
429
+
430
+
The C# compiler performs complex generation for string constant switches, making them extremely fast, and it would be difficult to achieve faster routing than this.
336
431
337
432
Parse and Value Binding
338
433
---
434
+
The method parameter names and types determine how to parse and bind values from the command-line arguments. When using lambda expressions, optional values and `params` arrays supported from C# 12 are also supported.
339
435
436
+
```csharp
437
+
ConsoleApp.Run(args, (
438
+
[Argument]DateTimedateTime, // Argument
439
+
[Argument]Guidguidvalue, //
440
+
intintVar, // required
441
+
boolboolFlag, // flag
442
+
MyEnumenumValue, // enum
443
+
int[] array, // array
444
+
MyClassobj, // object
445
+
stringoptional="abcde", // optional
446
+
double?nullableValue=null, // nullable
447
+
paramsstring[] paramsArray// params
448
+
) => { });
449
+
```
340
450
451
+
When using `ConsoleApp.Run`, you can check the syntax of the command line in the tooltip to see how it is generated.
// TODO:reason and policy of limitation of parsing
344
-
345
-
`[Argument]`
455
+
For the rules on converting parameter names to option names, aliases, and how to set documentation, refer to the [Option aliases](#option-aliases-and-help-version) section.
346
456
347
-
`bool`
457
+
Parameters marked with the `[Argument]` attribute receive values in order without parameter names. This attribute can only be set on sequential parameters from the beginning.
348
458
459
+
To convert from string arguments to various types, basic primitive types (`string`, `char`, `sbyte`, `byte`, `short`, `int`, `long`, `uint`, `ushort`, `ulong`, `decimal`, `float`, `double`) use `TryParse`. For types that implement `ISpanParsable<T>` (`DateTime`, `DateTimeOffset`, `Guid`, `BigInteger`, `Complex`, `Half`, `Int128`, etc.), [IParsable<TSelf>.TryParse](https://learn.microsoft.com/en-us/dotnet/api/system.iparsable-1.tryparse?view=net-8.0#system-ispanparsable-1-tryparse(system-readonlyspan((system-char))-system-iformatprovider-0@)) or [ISpanParsable<TSelf>.TryParse](https://learn.microsoft.com/en-us/dotnet/api/system.ispanparsable-1.tryparse?view=net-8.0#system-ispanparsable-1-tryparse(system-readonlyspan((system-char))-system-iformatprovider-0@)) is used.
349
460
461
+
For `enum`, it is parsed using `Enum.TryParse(ignoreCase: true)`.
350
462
463
+
`bool` is treated as a flag and is always optional. It becomes `true` when the parameter name is passed.
351
464
465
+
### Array
352
466
353
-
467
+
Array parsing has three special patterns.
354
468
469
+
For a regular `T[]`, if the value starts with `[`, it is parsed using `JsonSerialzier.Deserialize`. Otherwise, it is parsed as comma-separated values. For example, `[1,2,3]` or `1,2,3` are allowed as values. To set an empty array, pass `[]`.
355
470
356
-
`enum`
357
-
`nullable?`
358
-
`DateTime`
471
+
For `params T[]`, all subsequent arguments become the values of the array. For example, if there is an input like `--paramsArray foo bar baz`, it will be bound to a value like `["foo", "bar", "baz"]`.
359
472
360
-
`ISpanParsable<T>`
361
-
#### default
362
-
#### json
363
-
#### params T[]
473
+
### Object
364
474
475
+
If none of the above cases apply, `JsonSerializer.Deserialize<T>` is used to perform binding as JSON. However, `CancellationToken` and `ConsoleAppContext` are treated as special types and excluded from binding. Also, parameters with the `[FromServices]` attribute are not subject to binding.
365
476
366
-
####Custom Value Converter
477
+
### Custom Value Converter
367
478
368
-
// TODO:
479
+
To perform custom binding to existing types that do not support `ISpanParsable<T>`, you can create and set up a custom parser. For example, if you want to pass `System.Numerics.Vector3` as a comma-separated string like `1.3,4.12,5.947` and parse it, you can create an `Attribute` with `AttributeTargets.Parameter` that implements `IArgumentParser<T>`'s `static bool TryParse(ReadOnlySpan<char> s, out Vector3 result)` as follows:
369
480
370
481
```csharp
371
482
[AttributeUsage(AttributeTargets.Parameter)]
@@ -396,34 +507,76 @@ public class Vector3ParserAttribute : Attribute, IArgumentParser<Vector3>
396
507
}
397
508
```
398
509
510
+
By setting this attribute on a parameter, the custom parser will be called when parsing the args.
While there are some standards for command-line arguments, such as UNIX tools and POSIX, there is no absolute specification. The [Command-line syntax overview for System.CommandLine](https://learn.microsoft.com/en-us/dotnet/standard/commandline/syntax) provides an explanation of the specifications adopted by System.CommandLine. However, ConsoleAppFramework, while referring to these specifications to some extent, does not necessarily aim to fully comply with them.
400
519
520
+
For example, specifications that change behavior based on `-x` and `-X` or allow bundling `-f -d -x` as `-fdx` are not easy to understand and also take time to parse. The poor performance of System.CommandLine may be influenced by its adherence to complex grammar. Therefore, ConsoleAppFramework prioritizes performance and clear rules. It uses lower-kebab-case as the basis while allowing case-insensitive matching. It does not support ambiguous grammar that cannot be processed in a single pass or takes time to parse.
401
521
522
+
[System.CommandLine seems to be aiming for a new direction in .NET 9 and .NET 10](https://github.com/dotnet/command-line-api/issues/2338), but from a performance perspective, it will never surpass ConsoleAppFramework.
402
523
403
524
CancellationToken(Gracefully Shutdown) and Timeout
404
525
---
526
+
In ConsoleAppFramework, when you pass a `CancellationToken` as an argument, it can be used to check for interruption commands (SIGINT/SIGTERM/SIGKILL - Ctrl+C) rather than being treated as a parameter. For handling this, ConsoleAppFramework performs special code generation when a `CancellationToken` is included in the parameters.
If a CancellationToken is not passed, the application is immediately forced to terminate when an interruption command (Ctrl+C) is received. However, if a CancellationToken is present, it internally uses [`PosixSignalRegistration`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.posixsignalregistration) to hook SIGINT/SIGTERM/SIGKILL and sets the CancellationToken to a canceled state. Additionally, it prevents forced termination to allow for a graceful shutdown.
408
536
537
+
If the CancellationToken is handled correctly, the application can perform proper termination processing based on the application's handling. However, if the CancellationToken is mishandled, the application may not terminate even when an interruption command is received. To avoid this, a timeout timer starts after the interruption command, and the application is forcibly terminated again after the specified time.
409
538
410
-
Exit Code
411
-
---
412
-
If the method returns `int` or `Task<int>` or `ValueTask<int> value, ConsoleAppFramework will set the return value to the exit code.
539
+
The default timeout is 5 seconds, but it can be changed using `ConsoleApp.Timeout`. For example, setting it to `ConsoleApp.Timeout = Timeout.InfiniteTimeSpan;` disables the forced termination caused by the timeout.
413
540
541
+
The hooking behavior using `PosixSignalRegistration` is determined by the presence of a `CancellationToken` (or always takes effect if a filter is set). Therefore, even for synchronous methods, it is possible to change the behavior by including a `CancellationToken` as an argument.
414
542
543
+
Exit Code
544
+
---
545
+
If the method returns `int` or `Task<int>`, `ConsoleAppFramework` will set the return value to the exit code. Due to the nature of code generation, when writing lambda expressions, you need to explicitly specify either `int` or `Task<int>`.
415
546
416
-
> **NOTE**: If the method throws an unhandled exception, ConsoleAppFramework always set `1` to the exit code.
417
-
547
+
```csharp
548
+
// return Random ExitCode...
549
+
ConsoleApp.Run(args, int () =>Random.Shared.Next());
If the method throws an unhandled exception, ConsoleAppFramework always set `1` to the exit code. Also, in that case, output `Exception.ToString` to `ConsoleApp.LogError` (the default is `Console.WriteLine`). If you want to modify this code, please create a custom filter. For more details, refer to the [Filter](#filtermiddleware-pipline--consoleappcontext) section.
420
563
421
564
Attribute based parameters validation
422
565
---
566
+
`ConsoleAppFramework` performs validation when the parameters are marked with attributes for validation from `System.ComponentModel.DataAnnotations` (more precisely, attributes that implement `ValidationAttribute`). The validation occurs after parameter binding and before command execution. If the validation fails, it throws a `ValidationException`.
For example, if you pass arguments like `args = "--first-arg invalid.email --second-arg 10".Split(' ');`, you will see validation failure messages such as:
425
573
574
+
```txt
575
+
The firstArg field is not a valid e-mail address.
576
+
The field secondArg must be between 0 and 2.
577
+
```
426
578
579
+
By default, the ExitCode is set to 1 in this case.
427
580
428
581
Filter(Middleware) Pipline / ConsoleAppContext
429
582
---
@@ -786,10 +939,15 @@ internal class ServiceProviderScopeFilter(IServiceProvider serviceProvider, Cons
786
939
787
940
Publish to executable file
788
941
---
942
+
There are multiple ways to run a CLI application in .NET:
`run` is convenient when you want to execute the `csproj` directly, such as for starting command tools in CI. `build` and `publish` are quite similar, so it's possible to discuss them in general terms, but it's a bit difficult to talk about the precise differences. For more details, it's a good idea to check out [`build` vs `publish` -- can they be friends? · Issue #26247 · dotnet/sdk](https://github.com/dotnet/sdk/issues/26247).
789
949
790
-
* Native AOT
791
-
* dotnet run
792
-
* dotnet publish
950
+
Also, to run with Native AOT, please refer to the [Native AOT deployment overview](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/). In any case, ConsoleAppFramework thoroughly implements a dependency-free and reflection-free approach, so it shouldn't be an obstacle to execution.
0 commit comments