Skip to content

Commit a226bcd

Browse files
committed
Add ConsoleAppFramework.CliSchema
1 parent fdda0a8 commit a226bcd

File tree

11 files changed

+272
-15
lines changed

11 files changed

+272
-15
lines changed

ConsoleAppFramework.sln

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio Version 17
4-
VisualStudioVersion = 17.0.31903.59
3+
# Visual Studio Version 18
4+
VisualStudioVersion = 18.0.11205.157 d18.0
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1F399F98-7439-4F05-847B-CC1267B4B7F2}"
77
EndProject
@@ -18,8 +18,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
1818
.editorconfig = .editorconfig
1919
.gitignore = .gitignore
2020
Directory.Build.props = Directory.Build.props
21-
ReadMe.md = ReadMe.md
2221
exclusion.dic = exclusion.dic
22+
ReadMe.md = ReadMe.md
2323
EndProjectSection
2424
EndProject
2525
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleAppFramework", "src\ConsoleAppFramework\ConsoleAppFramework.csproj", "{09BEEA7B-B6D3-4011-BCAB-6DF976713695}"
@@ -38,6 +38,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FilterShareProject", "sandb
3838
EndProject
3939
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeAotTrimming", "tests\NativeAotTrimming\NativeAotTrimming.csproj", "{B14EB164-AC1E-4B0C-9CEB-312B233195E5}"
4040
EndProject
41+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppFramework.CliSchema", "src\ConsoleAppFramework.CliSchema\ConsoleAppFramework.CliSchema.csproj", "{52BC6A88-9699-420A-AAC3-A3C129EC7219}"
42+
EndProject
4143
Global
4244
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4345
Debug|Any CPU = Debug|Any CPU
@@ -144,6 +146,18 @@ Global
144146
{B14EB164-AC1E-4B0C-9CEB-312B233195E5}.Release|x64.Build.0 = Release|Any CPU
145147
{B14EB164-AC1E-4B0C-9CEB-312B233195E5}.Release|x86.ActiveCfg = Release|Any CPU
146148
{B14EB164-AC1E-4B0C-9CEB-312B233195E5}.Release|x86.Build.0 = Release|Any CPU
149+
{52BC6A88-9699-420A-AAC3-A3C129EC7219}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
150+
{52BC6A88-9699-420A-AAC3-A3C129EC7219}.Debug|Any CPU.Build.0 = Debug|Any CPU
151+
{52BC6A88-9699-420A-AAC3-A3C129EC7219}.Debug|x64.ActiveCfg = Debug|Any CPU
152+
{52BC6A88-9699-420A-AAC3-A3C129EC7219}.Debug|x64.Build.0 = Debug|Any CPU
153+
{52BC6A88-9699-420A-AAC3-A3C129EC7219}.Debug|x86.ActiveCfg = Debug|Any CPU
154+
{52BC6A88-9699-420A-AAC3-A3C129EC7219}.Debug|x86.Build.0 = Debug|Any CPU
155+
{52BC6A88-9699-420A-AAC3-A3C129EC7219}.Release|Any CPU.ActiveCfg = Release|Any CPU
156+
{52BC6A88-9699-420A-AAC3-A3C129EC7219}.Release|Any CPU.Build.0 = Release|Any CPU
157+
{52BC6A88-9699-420A-AAC3-A3C129EC7219}.Release|x64.ActiveCfg = Release|Any CPU
158+
{52BC6A88-9699-420A-AAC3-A3C129EC7219}.Release|x64.Build.0 = Release|Any CPU
159+
{52BC6A88-9699-420A-AAC3-A3C129EC7219}.Release|x86.ActiveCfg = Release|Any CPU
160+
{52BC6A88-9699-420A-AAC3-A3C129EC7219}.Release|x86.Build.0 = Release|Any CPU
147161
EndGlobalSection
148162
GlobalSection(SolutionProperties) = preSolution
149163
HideSolutionNode = FALSE
@@ -157,6 +171,7 @@ Global
157171
{855B0D28-DC69-470B-B3D9-481EE52737AA} = {1F399F98-7439-4F05-847B-CC1267B4B7F2}
158172
{2A1E8ED1-CEB9-47CB-8497-A0C4F5A8F025} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6}
159173
{B14EB164-AC1E-4B0C-9CEB-312B233195E5} = {AAD2D900-C305-4449-A9FC-6C7696FFEDFA}
174+
{52BC6A88-9699-420A-AAC3-A3C129EC7219} = {1F399F98-7439-4F05-847B-CC1267B4B7F2}
160175
EndGlobalSection
161176
GlobalSection(ExtensibilityGlobals) = postSolution
162177
SolutionGuid = {7F3E353A-C125-4020-8481-11DC6496358C}

ReadMe.md

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,11 @@ As you can see from the generated output, the help display is also fast. In typi
155155

156156
Getting Started
157157
--
158-
This library is distributed via NuGet, minimal requirement is .NET 8 and C# 13.
158+
This library is distributed via [NuGet](https://www.nuget.org/packages/ConsoleAppFramework), minimal requirement is .NET 8 and C# 13.
159159

160-
> dotnet add package [ConsoleAppFramework](https://www.nuget.org/packages/ConsoleAppFramework)
160+
```bash
161+
dotnet add package ConsoleAppFramework
162+
```
161163

162164
ConsoleAppFramework is an analyzer (Source Generator) and does not have any dll references. When referenced, the entry point class `ConsoleAppFramework.ConsoleApp` is generated internally.
163165

@@ -423,6 +425,33 @@ app.Run(args);
423425

424426
You can also combine this with `Add` or `Add<T>` to add more commands.
425427

428+
### Alias command
429+
430+
Similar to option aliases, commands also support aliases. In the `Add` method, separating `commandName` or `[Command]` with `|` defines them as aliases.
431+
432+
```csharp
433+
using ConsoleAppFramework;
434+
435+
var app = ConsoleApp.Create();
436+
437+
app.Add("build|b", () => { });
438+
app.Add("keyvault|kv", () => { });
439+
app.Add<Commands>();
440+
441+
app.Run(args);
442+
443+
public class Commands
444+
{
445+
/// <summary>Executes the check command using the specified coordinates.</summary>
446+
[Command("check|c")]
447+
public void Check() { }
448+
449+
/// <summary>Build this packages's and its dependencies' documenation.</summary>
450+
[Command("doc|d")]
451+
public void Doc() { }
452+
}
453+
```
454+
426455
### Performance of Commands
427456

428457
In `ConsoleAppFramework`, the number and types of registered commands are statically determined at compile time. For example, let's register the following four commands:
@@ -571,7 +600,15 @@ When using `ConsoleApp.Run`, you can check the syntax of the command line in the
571600

572601
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.
573602

574-
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.
603+
Parameters marked with the `[Argument]` attribute receive values in order without parameter names. This attribute allows from first or last.
604+
605+
```csharp
606+
// cmd.exe "input.txt" "output.txt"
607+
ConsoleApp.Run(args, ([Argument]string input, [Argument]string output, bool dryRun) => { });
608+
609+
// cmd.exe --message "foo bar baz" "output1.txt" "output2.txt" "output3.txt"
610+
ConsoleApp.Run(args, (string message, [Argument]params string[] outputs) => { });
611+
```
575612

576613
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.
577614

@@ -1428,6 +1465,55 @@ The framework doesn't support colorization directly; however, utilities like [Cy
14281465

14291466
For more powerful Console UI support, you can also use it in combination with [Spectre.Console](https://spectreconsole.net/).
14301467

1468+
Cli Schema
1469+
---
1470+
The dotnet command can output command and option information in JSON format using the `--cli-schema` option. In ConsoleAppFramework, by importing the `ConsoleAppFramework.CliSchema` package, you can call the `ConsoleAppBuilder.GetCliSchema()` method.
1471+
1472+
```bash
1473+
dotnet add package ConsoleAppFramework.CliSchema
1474+
```
1475+
1476+
```csharp
1477+
var app = ConsoleApp.Create();
1478+
app.Add("foo", (int x, int y) => { });
1479+
1480+
// get cli-schema as objects
1481+
CommandHelpDefinition[] schema = app.GetCliSchema();
1482+
```
1483+
1484+
`CommandHelpDefinition` is the same one used internally for help output.
1485+
1486+
```csharp
1487+
public record CommandHelpDefinition
1488+
{
1489+
public string CommandName { get; }
1490+
public CommandOptionHelpDefinition[] Options { get; }
1491+
public string Description { get; }
1492+
}
1493+
1494+
public record CommandOptionHelpDefinition
1495+
{
1496+
public string[] Options { get; }
1497+
public string Description { get; }
1498+
public string? DefaultValue { get; }
1499+
public string ValueTypeName { get; }
1500+
public int? Index { get; }
1501+
public bool IsRequired => DefaultValue == null && !IsParams;
1502+
public bool IsFlag { get; }
1503+
public bool IsParams { get; }
1504+
public bool IsHidden { get; }
1505+
public bool IsDefaultValueHidden { get; }
1506+
public string FormattedValueTypeName => "<" + ValueTypeName + ">";
1507+
}
1508+
```
1509+
1510+
You can also convert it to JSON. Since `CliSchemaJsonSerializerContext` is provided, by passing it as a Serialize/Deserialize argument, you can perform Native AOT-compatible conversion.
1511+
1512+
```csharp
1513+
var json = JsonSerializer.Serialize(schema, CliSchemaJsonSerializerContext.Default.CommandHelpDefinitionArray);
1514+
var schema2 = JsonSerializer.Deserialize(json, CliSchemaJsonSerializerContext.Default.CommandHelpDefinitionArray);
1515+
```
1516+
14311517
Publish to executable file
14321518
---
14331519
There are multiple ways to run a CLI application in .NET:
@@ -1440,6 +1526,8 @@ There are multiple ways to run a CLI application in .NET:
14401526

14411527
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.
14421528

1529+
There is a method of distributing CLI tools by packaging them as [.NET tools](https://learn.microsoft.com/en-us/dotnet/core/tools/global-tools). Also, starting from .NET 10, it is possible to execute them directly from the package manager using [dotnet tool exec(dnx)](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-tool-exec).
1530+
14431531
v4 -> v5 Migration Guide
14441532
---
14451533
v4 was running on top of `Microsoft.Extensions.Hosting`, so build a Host in the same way and need to convert ConsoleAppBuilder.

sandbox/GeneratorSandbox/GeneratorSandbox.csproj

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

3333
<ItemGroup>
3434
<ProjectReference Include="..\..\src\ConsoleAppFramework.Abstractions\ConsoleAppFramework.Abstractions.csproj" />
35+
<ProjectReference Include="..\..\src\ConsoleAppFramework.CliSchema\ConsoleAppFramework.CliSchema.csproj" />
3536
<ProjectReference Include="..\..\src\ConsoleAppFramework\ConsoleAppFramework.csproj">
3637
<OutputItemType>Analyzer</OutputItemType>
3738
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>

sandbox/GeneratorSandbox/Program.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
using ConsoleAppFramework;
22

3-
// args = ["foo", "--help"];
4-
53
var app = ConsoleApp.Create();
64

75
app.Add("build|b", () => { });
8-
app.Add("test|t", () => { });
96
app.Add("keyvault|kv", () => { });
107
app.Add<Commands>();
118

129
app.Run(args);
1310

1411
public class Commands
1512
{
16-
/// <summary>Analyze the current package and report errors, but don't build object files.</summary>
13+
/// <summary>
14+
/// Executes the check command using the specified coordinates.
15+
/// </summary>
1716
[Command("check|c")]
1817
public void Check() { }
1918

src/ConsoleAppFramework.Abstractions/ConsoleAppFramework.Abstractions.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net8.0</TargetFramework>
4+
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace ConsoleAppFramework;
4+
5+
public record CommandHelpDefinition
6+
{
7+
public string CommandName { get; }
8+
public CommandOptionHelpDefinition[] Options { get; }
9+
public string Description { get; }
10+
11+
public CommandHelpDefinition(string commandName, CommandOptionHelpDefinition[] options, string description)
12+
{
13+
CommandName = commandName;
14+
Options = options;
15+
Description = description;
16+
}
17+
}
18+
19+
public record CommandOptionHelpDefinition
20+
{
21+
public string[] Options { get; }
22+
public string Description { get; }
23+
public string? DefaultValue { get; }
24+
public string ValueTypeName { get; }
25+
public int? Index { get; }
26+
public bool IsRequired => DefaultValue == null && !IsParams;
27+
public bool IsFlag { get; }
28+
public bool IsParams { get; }
29+
public bool IsHidden { get; }
30+
public bool IsDefaultValueHidden { get; }
31+
public string FormattedValueTypeName => "<" + ValueTypeName + ">";
32+
33+
public CommandOptionHelpDefinition(string[] options, string description, string valueTypeName, string? defaultValue, int? index, bool isFlag, bool isParams, bool isHidden, bool isDefaultValueHidden)
34+
{
35+
Options = options;
36+
Description = description;
37+
ValueTypeName = valueTypeName;
38+
DefaultValue = defaultValue;
39+
Index = index;
40+
IsFlag = isFlag;
41+
IsParams = isParams;
42+
IsHidden = isHidden;
43+
IsDefaultValueHidden = isDefaultValueHidden;
44+
}
45+
}
46+
47+
[JsonSerializable(typeof(CommandHelpDefinition))]
48+
[JsonSerializable(typeof(CommandOptionHelpDefinition))]
49+
[JsonSerializable(typeof(CommandHelpDefinition[]))]
50+
[JsonSerializable(typeof(CommandOptionHelpDefinition[]))]
51+
[JsonSerializable(typeof(string[]))]
52+
[JsonSerializable(typeof(string))]
53+
[JsonSerializable(typeof(int))]
54+
[JsonSerializable(typeof(bool))]
55+
public partial class CliSchemaJsonSerializerContext : JsonSerializerContext { }
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<LangVersion>latest</LangVersion>
8+
9+
<!-- NuGet -->
10+
<IsPackable>true</IsPackable>
11+
<PackageId>ConsoleAppFramework.CliSchema</PackageId>
12+
<Description>ConsoleAppFramework cli-schema metadata library.</Description>
13+
<Configurations>Debug;Release</Configurations>
14+
</PropertyGroup>
15+
16+
</Project>

src/ConsoleAppFramework/CommandHelpBuilder.cs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Microsoft.CodeAnalysis;
1+
using Microsoft.CodeAnalysis;
22
using System.Text;
33

44
namespace ConsoleAppFramework;
@@ -47,6 +47,13 @@ public static string BuildCommandHelpMessage(Command command)
4747
return BuildHelpMessageCore(command, showCommandName: command.Name != "", showCommand: false);
4848
}
4949

50+
public static string BuildCliSchema(IEnumerable<Command> commands)
51+
{
52+
return "return new[] {\n"
53+
+ string.Join(", \n", commands.Select(x => CreateCommandHelpDefinition(x).ToCliSchema()))
54+
+ "\n};";
55+
}
56+
5057
static string BuildHelpMessageCore(Command command, bool showCommandName, bool showCommand)
5158
{
5259
var definition = CreateCommandHelpDefinition(command);
@@ -333,6 +340,44 @@ public CommandHelpDefinition(string command, CommandOptionHelpDefinition[] optio
333340
Options = options;
334341
Description = description;
335342
}
343+
344+
public string ToCliSchema()
345+
{
346+
var sb = new StringBuilder();
347+
sb.AppendLine($"new CommandHelpDefinition(");
348+
sb.AppendLine($" \"{EscapeString(CommandName)}\",");
349+
sb.AppendLine($" new CommandOptionHelpDefinition[]");
350+
sb.AppendLine($" {{");
351+
352+
for (int i = 0; i < Options.Length; i++)
353+
{
354+
sb.Append(" ");
355+
sb.Append(Options[i].ToCliSchema());
356+
if (i < Options.Length - 1)
357+
{
358+
sb.AppendLine(",");
359+
}
360+
else
361+
{
362+
sb.AppendLine();
363+
}
364+
}
365+
366+
sb.AppendLine($" }},");
367+
sb.AppendLine($" \"{EscapeString(Description)}\"");
368+
sb.Append($")");
369+
370+
return sb.ToString();
371+
}
372+
373+
private static string EscapeString(string str)
374+
{
375+
return str.Replace("\\", "\\\\")
376+
.Replace("\"", "\\\"")
377+
.Replace("\n", "\\n")
378+
.Replace("\r", "\\r")
379+
.Replace("\t", "\\t");
380+
}
336381
}
337382

338383
class CommandOptionHelpDefinition
@@ -361,5 +406,23 @@ public CommandOptionHelpDefinition(string[] options, string description, string
361406
IsHidden = isHidden;
362407
IsDefaultValueHidden = isDefaultValueHidden;
363408
}
409+
410+
public string ToCliSchema()
411+
{
412+
var optionsArray = string.Join(", ", Options.Select(o => $"\"{EscapeString(o)}\""));
413+
var defaultValueStr = DefaultValue == null ? "null" : $"\"{EscapeString(DefaultValue)}\"";
414+
var indexStr = Index.HasValue ? Index.Value.ToString() : "null";
415+
416+
return $"new CommandOptionHelpDefinition(new[] {{ {optionsArray} }}, \"{EscapeString(Description)}\", \"{EscapeString(ValueTypeName)}\", {defaultValueStr}, {indexStr}, {IsFlag.ToString().ToLower()}, {IsParams.ToString().ToLower()}, {IsHidden.ToString().ToLower()}, {IsDefaultValueHidden.ToString().ToLower()})";
417+
}
418+
419+
private static string EscapeString(string str)
420+
{
421+
return str.Replace("\\", "\\\\")
422+
.Replace("\"", "\\\"")
423+
.Replace("\n", "\\n")
424+
.Replace("\r", "\\r")
425+
.Replace("\t", "\\t");
426+
}
364427
}
365428
}

0 commit comments

Comments
 (0)