Skip to content

Commit b551bbd

Browse files
Add OpenCLI integration to Spectre.Console.Cli
1 parent 3a70fbe commit b551bbd

20 files changed

Lines changed: 549 additions & 27 deletions

docs/input/cli/opencli.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Title: OpenCLI Integration
2+
Order: 15
3+
Description: OpenCLI integration
4+
Highlights:
5+
- Generate OpenCLI descriptions
6+
---
7+
8+
From version `0.52.0` and above, you will be able to generate [OpenCLI](https://opencli.org)
9+
descriptions from your `Spectre.Console.Cli` applications.
10+
11+
Simply add the `--help-dump-opencli` option to your application, and an
12+
OpenCLI description will be written to stdout.
13+
14+
```shell
15+
$ ./myapp --help-dump-opencli
16+
```
17+
18+
If you want to save it to disk, pipe it to a file.
19+
20+
```shell
21+
$ ./myapp --help-dump-opencli > myapp.openapi.json
22+
```

src/Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
99
<PackageVersion Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" Version="8.0.0" />
1010
<PackageVersion Include="MinVer" PrivateAssets="All" Version="6.0.0" />
11+
<PackageVersion Include="OpenCli.Sources" Version="0.5.0" />
1112
<PackageVersion Include="PolySharp" Version="1.15.0" />
1213
<PackageVersion Include="Roslynator.Analyzers" PrivateAssets="All" Version="4.14.0" />
1314
<PackageVersion Include="Shouldly" Version="4.3.0" />

src/Spectre.Console.Cli/CommandApp.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ public async Task<int> RunAsync(IEnumerable<string> args)
7979
cli.AddCommand<VersionCommand>(CliConstants.Commands.Version);
8080
cli.AddCommand<XmlDocCommand>(CliConstants.Commands.XmlDoc);
8181
cli.AddCommand<ExplainCommand>(CliConstants.Commands.Explain);
82+
cli.AddCommand<OpenCliGeneratorCommand>(CliConstants.Commands.OpenCli);
8283
});
8384

8485
_executed = true;

src/Spectre.Console.Cli/Internal/CommandExecutor.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@ public async Task<int> Execute(IConfiguration configuration, IEnumerable<string>
6868
}
6969
}
7070
}
71+
72+
// OpenCLI?
73+
if (firstArgument.Equals(CliConstants.DumpHelpOpenCliOption, StringComparison.OrdinalIgnoreCase))
74+
{
75+
// Replace all arguments with the opencligen command
76+
arguments = ["cli", "opencli"];
77+
}
7178
}
7279

7380
// Parse and map the model against the arguments.

src/Spectre.Console.Cli/Internal/Commands/ExplainCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ namespace Spectre.Console.Cli;
22

33
[Description("Displays diagnostics about CLI configurations")]
44
[SuppressMessage("Performance", "CA1812: Avoid uninstantiated internal classes")]
5-
internal sealed class ExplainCommand : Command<ExplainCommand.Settings>
5+
internal sealed class ExplainCommand : Command<ExplainCommand.Settings>, IBuiltInCommand
66
{
77
private readonly CommandModel _commandModel;
88
private readonly IAnsiConsole _writer;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Spectre.Console.Cli;
2+
3+
/// <summary>
4+
/// Represents a built-in command.
5+
/// Used as a marker interface.
6+
/// </summary>
7+
internal interface IBuiltInCommand : ICommand
8+
{
9+
}
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
using OpenCli;
2+
3+
namespace Spectre.Console.Cli;
4+
5+
internal sealed class OpenCliGeneratorCommand : Command, IBuiltInCommand
6+
{
7+
private readonly IConfiguration _configuration;
8+
private readonly CommandModel _model;
9+
10+
public OpenCliGeneratorCommand(IConfiguration configuration, CommandModel model)
11+
{
12+
_configuration = configuration;
13+
_model = model ?? throw new ArgumentNullException(nameof(model));
14+
}
15+
16+
public override int Execute(CommandContext context)
17+
{
18+
var document = new OpenCliDocument
19+
{
20+
OpenCli = "0.1-draft",
21+
Info = new OpenCliInfo
22+
{
23+
Title = ((ICommandModel)_model).ApplicationName, Version = _model.ApplicationVersion ?? "1.0",
24+
},
25+
Commands = CreateCommands(_model.Commands),
26+
Arguments = CreateArguments(_model.DefaultCommand?.GetArguments()),
27+
Options = CreateOptions(_model.DefaultCommand?.GetOptions()),
28+
};
29+
30+
var writer = _configuration.Settings.Console.GetConsole();
31+
writer.WriteLine(document.Write());
32+
33+
return 0;
34+
}
35+
36+
private List<OpenCliCommand> CreateCommands(IList<CommandInfo> commands)
37+
{
38+
var result = new List<OpenCliCommand>();
39+
40+
foreach (var command in commands.OrderBy(o => o.Name, StringComparer.OrdinalIgnoreCase))
41+
{
42+
if (typeof(IBuiltInCommand).IsAssignableFrom(command.CommandType))
43+
{
44+
continue;
45+
}
46+
47+
var openCliCommand = new OpenCliCommand
48+
{
49+
Name = command.Name,
50+
Aliases =
51+
[
52+
..command.Aliases.OrderBy(str => str)
53+
],
54+
Commands = CreateCommands(command.Children),
55+
Arguments = CreateArguments(command.GetArguments()),
56+
Options = CreateOptions(command.GetOptions()),
57+
Description = command.Description,
58+
Hidden = command.IsHidden,
59+
Examples = [..command.Examples.Select(example => string.Join(" ", example))],
60+
};
61+
62+
// Skip branches without commands
63+
if (command.IsBranch && openCliCommand.Commands.Count == 0)
64+
{
65+
continue;
66+
}
67+
68+
result.Add(openCliCommand);
69+
}
70+
71+
return result;
72+
}
73+
74+
private List<OpenCliArgument> CreateArguments(IEnumerable<CommandArgument>? arguments)
75+
{
76+
var result = new List<OpenCliArgument>();
77+
78+
if (arguments == null)
79+
{
80+
return result;
81+
}
82+
83+
foreach (var argument in arguments.OrderBy(x => x.Position))
84+
{
85+
var metadata = default(List<OpenCliMetadata>);
86+
if (argument.ParameterType != typeof(void) &&
87+
argument.ParameterType != typeof(bool))
88+
{
89+
metadata =
90+
[
91+
new OpenCliMetadata { Name = "ClrType", Value = argument.ParameterType.ToCliTypeString(), },
92+
];
93+
}
94+
95+
result.Add(new OpenCliArgument
96+
{
97+
Name = argument.Value,
98+
Required = argument.IsRequired,
99+
Arity = new OpenCliArity
100+
{
101+
// TODO: Look into this
102+
Minimum = 1,
103+
Maximum = 1,
104+
},
105+
Description = argument.Description,
106+
Hidden = argument.IsHidden,
107+
Metadata = metadata,
108+
AcceptedValues = null,
109+
Group = null,
110+
});
111+
}
112+
113+
return result;
114+
}
115+
116+
private List<OpenCliOption> CreateOptions(IEnumerable<CommandOption>? options)
117+
{
118+
var result = new List<OpenCliOption>();
119+
120+
if (options == null)
121+
{
122+
return result;
123+
}
124+
125+
foreach (var option in options.OrderBy(o => o.GetOptionName(), StringComparer.OrdinalIgnoreCase))
126+
{
127+
var arguments = new List<OpenCliArgument>();
128+
if (option.ParameterType != typeof(void) &&
129+
option.ParameterType != typeof(bool))
130+
{
131+
arguments.Add(new OpenCliArgument
132+
{
133+
Name = option.ValueName ?? "VALUE",
134+
Required = !option.ValueIsOptional,
135+
Arity = new OpenCliArity
136+
{
137+
// TODO: Look into this
138+
Minimum = option.ValueIsOptional
139+
? 0
140+
: 1,
141+
Maximum = 1,
142+
},
143+
AcceptedValues = null,
144+
Group = null,
145+
Hidden = null,
146+
Metadata =
147+
[
148+
new OpenCliMetadata
149+
{
150+
Name = "ClrType",
151+
Value = option.ParameterType.ToCliTypeString(),
152+
},
153+
],
154+
});
155+
}
156+
157+
var optionMetadata = default(List<OpenCliMetadata>);
158+
if (arguments.Count == 0 && option.ParameterType != typeof(void) &&
159+
option.ParameterType != typeof(bool))
160+
{
161+
optionMetadata =
162+
[
163+
new OpenCliMetadata { Name = "ClrType", Value = option.ParameterType.ToCliTypeString(), },
164+
];
165+
}
166+
167+
var (optionName, optionAliases) = GetOptionNames(option);
168+
result.Add(new OpenCliOption
169+
{
170+
Name = optionName,
171+
Required = option.IsRequired,
172+
Aliases = [..optionAliases.OrderBy(str => str)],
173+
Arguments = arguments,
174+
Description = option.Description,
175+
Group = null,
176+
Hidden = option.IsHidden,
177+
Recursive = option.IsShadowed, // TODO: Is this correct?
178+
Metadata = optionMetadata,
179+
});
180+
}
181+
182+
return result;
183+
}
184+
185+
private static (string Name, HashSet<string> Aliases) GetOptionNames(CommandOption option)
186+
{
187+
var name = GetOptionName(option);
188+
var aliases = new HashSet<string>();
189+
190+
if (option.LongNames.Count > 0)
191+
{
192+
foreach (var alias in option.LongNames.Skip(1))
193+
{
194+
aliases.Add("--" + alias);
195+
}
196+
197+
foreach (var alias in option.ShortNames)
198+
{
199+
aliases.Add("-" + alias);
200+
}
201+
}
202+
else
203+
{
204+
foreach (var alias in option.LongNames)
205+
{
206+
aliases.Add("--" + alias);
207+
}
208+
209+
foreach (var alias in option.ShortNames.Skip(1))
210+
{
211+
aliases.Add("-" + alias);
212+
}
213+
}
214+
215+
return (name, aliases);
216+
}
217+
218+
private static string GetOptionName(CommandOption option)
219+
{
220+
return option.LongNames.Count > 0
221+
? "--" + option.LongNames[0]
222+
: "-" + option.ShortNames[0];
223+
}
224+
}

src/Spectre.Console.Cli/Internal/Commands/VersionCommand.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ namespace Spectre.Console.Cli;
22

33
[Description("Displays the CLI library version")]
44
[SuppressMessage("Performance", "CA1812: Avoid uninstantiated internal classes")]
5-
internal sealed class VersionCommand : Command<VersionCommand.Settings>
5+
internal sealed class VersionCommand : Command, IBuiltInCommand
66
{
77
private readonly IAnsiConsole _writer;
88

@@ -11,11 +11,7 @@ public VersionCommand(IConfiguration configuration)
1111
_writer = configuration.Settings.Console.GetConsole();
1212
}
1313

14-
public sealed class Settings : CommandSettings
15-
{
16-
}
17-
18-
public override int Execute(CommandContext context, Settings settings)
14+
public override int Execute(CommandContext context)
1915
{
2016
_writer.MarkupLine(
2117
"[yellow]Spectre.Cli[/] version [aqua]{0}[/]",

src/Spectre.Console.Cli/Internal/Commands/XmlDocCommand.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ namespace Spectre.Console.Cli;
22

33
[Description("Generates an XML representation of the CLI configuration.")]
44
[SuppressMessage("Performance", "CA1812: Avoid uninstantiated internal classes")]
5-
internal sealed class XmlDocCommand : Command<XmlDocCommand.Settings>
5+
internal sealed class XmlDocCommand : Command, IBuiltInCommand
66
{
77
private readonly CommandModel _model;
88
private readonly IAnsiConsole _writer;
@@ -13,11 +13,7 @@ public XmlDocCommand(IConfiguration configuration, CommandModel model)
1313
_writer = configuration.Settings.Console.GetConsole();
1414
}
1515

16-
public sealed class Settings : CommandSettings
17-
{
18-
}
19-
20-
public override int Execute(CommandContext context, Settings settings)
16+
public override int Execute(CommandContext context)
2117
{
2218
_writer.Write(Serialize(_model), Style.Plain);
2319
return 0;

src/Spectre.Console.Cli/Internal/Constants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ internal static class CliConstants
55
public const string DefaultCommandName = "__default_command";
66
public const string True = "true";
77
public const string False = "false";
8+
public const string DumpHelpOpenCliOption = "--help-dump-opencli";
89

910
public static string[] AcceptedBooleanValues { get; } = new string[]
1011
{
@@ -18,5 +19,6 @@ public static class Commands
1819
public const string Version = "version";
1920
public const string XmlDoc = "xmldoc";
2021
public const string Explain = "explain";
22+
public const string OpenCli = "opencli";
2123
}
2224
}

0 commit comments

Comments
 (0)