Skip to content

Commit 671bf68

Browse files
committed
done
1 parent e8e9617 commit 671bf68

File tree

8 files changed

+200
-68
lines changed

8 files changed

+200
-68
lines changed

ReadMe.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,27 @@ Unfortunately, due to current C# specifications, lambda expressions and [local f
269269

270270
In addition to `-h|--help`, there is another special built-in option: `--version`. In default, it displays the `AssemblyInformationalVersion` without source revision or `AssemblyVersion`. You can configure version string by `ConsoleApp.Version`, for example `ConsoleApp.Version = "2001.9.3f14-preview2";`.
271271

272+
When a default value exists, that value is displayed by default. If you want to omit this display, add the `[HideDefaultValue]` attribute.
273+
274+
```csharp
275+
ConsoleApp.Run(args, (Fruit myFruit = Fruit.Apple, [HideDefaultValue] Fruit myFruit2 = Fruit.Grape) => { });
276+
277+
enum Fruit
278+
{
279+
Orange, Grape, Apple
280+
}
281+
```
282+
283+
```txt
284+
Usage: [options...] [-h|--help] [--version]
285+
286+
Options:
287+
--my-fruit <Fruit> (Default: Apple)
288+
--my-fruit2 <Fruit>
289+
```
290+
291+
Additionally, if you add the `[Hidden]` attribute, the help for that parameter itself will be hidden.
292+
272293
Command
273294
---
274295
If you want to register multiple commands or perform complex operations (such as adding filters), instead of using `Run/RunAsync`, obtain the `ConsoleAppBuilder` using `ConsoleApp.Create()`. Call `Add`, `Add<T>`, or `UseFilter<T>` multiple times on the `ConsoleAppBuilder` to register commands and filters, and finally execute the application using `Run` or `RunAsync`.
@@ -574,6 +595,93 @@ If you want to change the deserialization options, you can set `JsonSerializerOp
574595

575596
> NOTE: If they are not set when NativeAOT is used, a runtime exception may occur. If they are included in the parsing process, be sure to set source generated options.
576597
598+
### GlobalOptions
599+
600+
By calling `ConfigureGlobalOptions` on `ConsoleAppBuilder`, you can define global options that are enabled for all commands. For example, `--dry-run` or `--verbose` are suitable as common options.
601+
602+
```csharp
603+
var app = ConsoleApp.Create();
604+
// builder: Func<ref ConsoleApp.GlobalOptionsBuilder, object>
605+
app.ConfigureGlobalOptions((ref builder) =>
606+
{
607+
var dryRun = builder.AddGlobalOption<bool>("--dry-run");
608+
var verbose = builder.AddGlobalOption<bool>("-v|--verbose");
609+
var intParameter = builder.AddRequiredGlobalOption<int>("--int-parameter", "integer parameter");
610+
// return value stored to ConsoleAppContext.GlobalOptions
611+
return new GlobalOptions(dryRun, verbose, intParameter);
612+
});
613+
app.Add("", (int x, int y, ConsoleAppContext context) =>
614+
{
615+
var globalOptions = (GlobalOptions)context.GlobalOptions;
616+
Console.WriteLine(globalOptions + ":" + (x, y));
617+
});
618+
app.Run(args);
619+
internal record GlobalOptions(bool DryRun, bool Verbose, int IntParameter);
620+
```
621+
622+
> NOTE: `(ref builder)` can be written in C# 14 and later. For earlier versions, you need to write `(ref ConsoleApp.GlobalOptionsBuilder builder)`.
623+
624+
With `AddGlobalOption<T>`, you can define optional global options, and with `AddRequiredGlobalOption<T>`, you can define required global options. Each of these is also reflected in the command's help.
625+
626+
To define name aliases, separate them with `|` like `-v|--verbose`.
627+
628+
Unlike command parameters, the supported types are limited to types that can define C# compile-time constants (`string`, `char`, `sbyte`, `byte`, `short`, `int`, `long`, `uint`, `ushort`, `ulong`, `decimal`, `float`, `double`, `Enum`) and their `Nullable<T>` only.
629+
630+
Parsed global options can be retrieved from `ConsoleAppContext.GlobalOptions`. Additionally, when combined with DI, you can retrieve them in a typed manner in each command.
631+
632+
```csharp
633+
var app = ConsoleApp.Create();
634+
635+
// builder: Func<ref ConsoleApp.GlobalOptionsBuilder, object>
636+
app.ConfigureGlobalOptions((ref builder) =>
637+
{
638+
var dryRun = builder.AddGlobalOption<bool>("--dry-run");
639+
var verbose = builder.AddGlobalOption<bool>("-v|--verbose");
640+
var intParameter = builder.AddRequiredGlobalOption<int>("--int-parameter", "integer parameter");
641+
642+
// return value stored to ConsoleAppContext.GlobalOptions
643+
return new GlobalOptions(dryRun, verbose, intParameter);
644+
});
645+
646+
app.ConfigureServices((context, configuration, services) =>
647+
{
648+
// store global-options to DI
649+
var globalOptions = (GlobalOptions)context.GlobalOptions;
650+
services.AddSingleton(globalOptions);
651+
652+
// check global-options value to configure services
653+
services.AddLogging(logging =>
654+
{
655+
if (globalOptions.Verbose)
656+
{
657+
logging.SetMinimumLevel(LogLevel.Trace);
658+
}
659+
});
660+
});
661+
662+
app.Add<Commands>();
663+
664+
app.Run(args);
665+
666+
internal record GlobalOptions(bool DryRun, bool Verbose, int IntParameter);
667+
668+
// get GlobalOptions from DI
669+
internal class Commands(GlobalOptions globalOptions)
670+
{
671+
[Command("cmd-a")]
672+
public void CommandA(int x, int y)
673+
{
674+
Console.WriteLine("A:" + globalOptions + ":" + (x, y));
675+
}
676+
677+
[Command("cmd-b")]
678+
public void CommandB(int x, int y)
679+
{
680+
Console.WriteLine("B:" + globalOptions + ":" + (x, y));
681+
}
682+
}
683+
```
684+
577685
### Custom Value Converter
578686

579687
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:
@@ -1074,6 +1182,8 @@ When `Microsoft.Extensions.Configuration` is imported, `ConfigureEmptyConfigurat
10741182

10751183
Furthermore, overloads of `Action<IConfiguration, IServiceCollection> configure` and `Action<IConfiguration, ILoggingBuilder> configure` are added to `ConfigureServices` and `ConfigureLogging`, allowing you to retrieve the Configuration when executing the delegate.
10761184

1185+
There are also overloads that receive ConsoleAppContext. If you want to obtain global options and build with them, you can use these overloads: `Action<ConsoleAppContext, IConfiguration, IServiceCollection>` and `Action<ConsoleAppContext, IConfiguration, ILoggingBuilder>`.
1186+
10771187
without Hosting dependency, I've preferred these import packages.
10781188

10791189
```xml

sandbox/GeneratorSandbox/GeneratorSandbox.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
55
<TargetFramework>net8.0</TargetFramework>
6-
<LangVersion>13</LangVersion>
6+
<LangVersion>preview</LangVersion>
77

88
<ImplicitUsings>enable</ImplicitUsings>
99
<Nullable>disable</Nullable>

sandbox/GeneratorSandbox/Program.cs

Lines changed: 26 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -5,95 +5,55 @@
55
using System.Diagnostics.CodeAnalysis;
66
using System.Runtime.CompilerServices;
77

8-
//args = ["--x", "10", "--y", "20", "-v", "--prefix-output", "takoyakix"];
8+
args = ["cmd-a", "--x", "10", "--y", "20", "--int-parameter", "1000"];
99

1010
var app = ConsoleApp.Create();
1111

12-
13-
14-
//
15-
// AddGlobalOption
16-
17-
app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) =>
12+
// builder: Func<ref ConsoleApp.GlobalOptionsBuilder, object>
13+
app.ConfigureGlobalOptions((ref builder) =>
1814
{
19-
// builder.AddGlobalOption(description: "hoge", defaultValue: 0, "tako");
20-
21-
var verbose = builder.AddGlobalOption<bool>($"-v", "");
22-
var noColor = builder.AddGlobalOption<bool>("--no-color", "Don't colorize output.");
23-
var dryRun = builder.AddGlobalOption<bool>("--dry-run", "");
24-
25-
var dame = builder.AddGlobalOption<int>("hoge", "huga", 0);
26-
27-
var takoyaki = builder.AddGlobalOption<int?>("hogemoge", defaultValue: null);
28-
29-
var prefixOutput = builder.AddRequiredGlobalOption<string>(description: "Prefix output with level.", name: "--prefix-output|-pp|-po");
15+
var dryRun = builder.AddGlobalOption<bool>("--dry-run");
16+
var verbose = builder.AddGlobalOption<bool>("-v|--verbose");
17+
var intParameter = builder.AddRequiredGlobalOption<int>("--int-parameter", "integer parameter");
3018

31-
// var tako = builder.AddGlobalOption<int>("--in", "");
32-
//var tako = builder.AddGlobalOption<MyFruit>("--fruit", "");
33-
34-
//return new GlobalOptions(true, true, true, "");
35-
return new GlobalOptions(verbose, noColor, dryRun, prefixOutput);
19+
// return value stored to ConsoleAppContext.GlobalOptions
20+
return new GlobalOptions(dryRun, verbose, intParameter);
3621
});
3722

38-
app.ConfigureServices((context, configuration, collection) =>
23+
app.ConfigureServices((context, configuration, services) =>
3924
{
25+
// store global-options to DI
4026
var globalOptions = (GlobalOptions)context.GlobalOptions;
27+
services.AddSingleton(globalOptions);
4128

42-
// simply use for filter/command body
43-
collection.AddSingleton(globalOptions);
44-
45-
// variable for setup other DI
46-
collection.AddLogging(logging =>
29+
// check global-options value to configure services
30+
services.AddLogging(logging =>
4731
{
48-
var console = logging.AddSimpleConsole();
4932
if (globalOptions.Verbose)
5033
{
51-
console.SetMinimumLevel(LogLevel.Trace);
34+
logging.SetMinimumLevel(LogLevel.Trace);
5235
}
5336
});
5437
});
5538

56-
app.Add<MyCommand>();
57-
58-
// app.Add("", (int x, int y, [FromServices] GlobalOptions globalOptions) => Console.WriteLine(x + y + ":" + globalOptions));
59-
60-
//var iii = int.Parse("1000");
61-
//var sss = new string('a', 3);
62-
//var datet = DateTime.Parse("10000");
63-
64-
// AddGlobalOption<int?>("hoge", "takoyaki", null);
39+
app.Add<Commands>();
6540

6641
app.Run(args);
6742

43+
internal record GlobalOptions(bool DryRun, bool Verbose, int IntParameter);
6844

69-
// public T AddGlobalOption<T>([ConstantExpected] string name, [ConstantExpected] string description = "", T defaultValue = default(T))
70-
71-
72-
static void AddGlobalOption<T>([ConstantExpected] string name, [ConstantExpected] string description, [ConstantExpected] T defaultValue)
45+
// get GlobalOptions from DI
46+
internal class Commands(GlobalOptions globalOptions)
7347
{
74-
}
75-
76-
internal record GlobalOptions(bool Verbose, bool NoColor, bool DryRun, string PrefixOutput);
77-
78-
internal class MyCommand(GlobalOptions globalOptions)
79-
{
80-
/// <summary>
81-
/// my command
82-
/// </summary>
83-
[Command("")]
84-
public void Run(int x)
48+
[Command("cmd-a")]
49+
public void CommandA(int x, int y)
8550
{
86-
Console.WriteLine(globalOptions);
51+
Console.WriteLine("A:" + globalOptions + ":" + (x, y));
8752
}
88-
}
89-
90-
public enum MyFruit
91-
{
92-
Apple, Orange, Grape
93-
}
94-
95-
96-
public class Takoyaki
97-
{
9853

54+
[Command("cmd-b")]
55+
public void CommandB(int x, int y)
56+
{
57+
Console.WriteLine("B:" + globalOptions + ":" + (x, y));
58+
}
9959
}

src/ConsoleAppFramework/Command.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ public record class CommandParameter
155155
public required bool IsNullableReference { get; init; }
156156
public required bool IsParams { get; init; }
157157
public required bool IsHidden { get; init; } // Hide command parameter help
158-
public bool IsDefaultValueHidden { get; init; } = false; // Hide default value in command parameter help
158+
public required bool IsDefaultValueHidden { get; init; } // Hide default value in command parameter help
159159
public required string Name { get; init; }
160160
public required string OriginalParameterName { get; init; }
161161
public required bool HasDefaultValue { get; init; }
@@ -491,6 +491,7 @@ public CommandParameter ToDummyCommandParameter()
491491
IsCancellationToken = false,
492492
HasValidation = false,
493493
ArgumentIndex = -1,
494+
IsDefaultValueHidden = false,
494495

495496
Type = Type,
496497
HasDefaultValue = !IsRequired, // if not required, needs defaultValue

src/ConsoleAppFramework/CommandHelpBuilder.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,9 @@ static string BuildOptionsMessage(CommandHelpDefinition definition)
203203
else if (opt.DefaultValue != null)
204204
{
205205
if (!opt.IsDefaultValueHidden)
206+
{
206207
sb.Append($" (Default: {opt.DefaultValue})");
208+
}
207209
}
208210
else if (opt.IsRequired)
209211
{

src/ConsoleAppFramework/Parser.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Microsoft.CodeAnalysis.CSharp.Syntax;
44
using System.Collections.Immutable;
55
using System.Runtime.InteropServices.ComTypes;
6+
using System.Xml.Linq;
67

78
namespace ConsoleAppFramework;
89

@@ -452,6 +453,10 @@ bool IsParsableType(ITypeSymbol type)
452453
.SelectMany(x => x.Attributes)
453454
.Any(x => model.GetTypeInfo(x).Type?.Name == "HiddenAttribute");
454455

456+
var isDefaultValueHidden = x.AttributeLists
457+
.SelectMany(x => x.Attributes)
458+
.Any(x => model.GetTypeInfo(x).Type?.Name == "HideDefaultValueAttribute");
459+
455460
var customParserType = x.AttributeLists.SelectMany(x => x.Attributes)
456461
.Select(x =>
457462
{
@@ -562,6 +567,7 @@ bool IsParsableType(ITypeSymbol type)
562567
IsConsoleAppContext = isConsoleAppContext,
563568
IsParams = hasParams,
564569
IsHidden = isHidden,
570+
IsDefaultValueHidden = isDefaultValueHidden,
565571
Type = new EquatableTypeSymbol(type.Type!),
566572
Location = x.GetLocation(),
567573
HasDefaultValue = hasDefault,

tests/ConsoleAppFramework.GeneratorTests/GlobalOptionTest.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,23 @@ public void RequiredParse()
131131

132132
error.Contains("Required argument '--parameter' was not specified.");
133133
}
134+
135+
[Fact]
136+
public void NamedParameter()
137+
{
138+
verifier.Execute("""
139+
var app = ConsoleApp.Create();
140+
app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) =>
141+
{
142+
var p = builder.AddGlobalOption<int>("--parameter", defaultValue: 1000);
143+
var d = builder.AddGlobalOption<bool>(description: "foo", name: "--dry-run");
144+
return (p, d);
145+
});
146+
app.Add("", (int x, int y, ConsoleAppContext context) =>
147+
{
148+
Console.Write($"{context.GlobalOptions} -> {(x, y)}");
149+
});
150+
app.Run(args);
151+
""", "--x 10 --dry-run --y 20", "(1000, True) -> (10, 20)");
152+
}
134153
}

tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,40 @@ Display Hello.
344344
Options:
345345
-m|--message <string> Message to show.
346346
347+
""");
348+
}
349+
350+
[Fact]
351+
public void GlobalOptions()
352+
{
353+
var code = """
354+
var app = ConsoleApp.Create();
355+
356+
app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) =>
357+
{
358+
var p = builder.AddGlobalOption<int>("--parameter", "param global", defaultValue: 1000);
359+
var d = builder.AddGlobalOption<bool>(description: "run dry dry", name: "--dry-run");
360+
var r = builder.AddRequiredGlobalOption<int>("--p2|--p3", "param 2");
361+
return (p, d, r);
362+
});
363+
364+
app.Add("", (int x, int y) => { });
365+
app.Add("a", (int x, int y) => { });
366+
app.Add("ab", (int x, int y) => { });
367+
app.Add("a b c", (int x, int y) => { });
368+
app.Run(args);
369+
""";
370+
371+
verifier.Execute(code, args: "a --help", expected: """
372+
Usage: a [options...] [-h|--help] [--version]
373+
374+
Options:
375+
--x <int> (Required)
376+
--y <int> (Required)
377+
--parameter <int> param global (Default: 1000)
378+
--dry-run run dry dry (Optional)
379+
--p2|--p3 <int> param 2 (Required)
380+
347381
""");
348382
}
349383

0 commit comments

Comments
 (0)