feat!: refactored to handle updates to System.CommandLine
This commit is contained in:
parent
80976256fb
commit
0298ae601e
22 changed files with 416 additions and 529 deletions
29
README.md
29
README.md
|
@ -1,4 +1,27 @@
|
|||
Nitride CIL
|
||||
===========
|
||||
# MfGames.ToolBuilder
|
||||
|
||||
A static site generator based on the [Gallium ECS](https://gitlab.com/mfgames-cil/gallium-cil/).
|
||||
_An opinionated library for easily creating command-line tools in C#._
|
||||
|
||||
ToolBuilder is a library to encapsulate the setup and running of tools, CLI components arranged with verbs (like `git`). It includes some opinionated decisions on default setup.
|
||||
|
||||
## Commands
|
||||
|
||||
This library is built on top of [System.CommandLine](https://github.com/dotnet/command-line-api) and .NET 6 generic hosting. The commands are combined together using dependency injection (see below) which allows for them to be included as parameters for the constructor or dynamically discovered.
|
||||
|
||||
All commands are assumed to be asynchronous and include a cancellation token in their calls.
|
||||
|
||||
`System.CommandLine` was chosen because it allows for composition of features instead on needing inheritance to give a tool the ability to render a table or handle additional features.
|
||||
|
||||
## Autofac
|
||||
|
||||
While most libraries should only use the included service provider infrastructure with the base library, this tool sets up [Autofac](https://autofac.org/) to handle services. This allows for both service provider usage (via `ConfigureServices`) and Autofac (via `ConfigureContainer`).
|
||||
|
||||
Autofac was chosen because of a number of quality of life features and personal preferences of the developer. A tool can easily ignore that functionality in favor of using only `IServiceProvider`.
|
||||
|
||||
## Serilog
|
||||
|
||||
Likewise, tools are set up to use [Serilog](https://serilog.net/) for logging instead of the built-in logging code. However, both are served by Serilog so `ILoggerFactory<T>` can be used.
|
||||
|
||||
Like Autofac, Serilog was given because of the variety of sinks, certain quality of life, but also the ability to log more detailed objects. It also produces some coloring to log details based on data types.
|
||||
|
||||
To use the Serilog functionality with `ILoggerFactory`, objects needed to be included as parameters instead of inlined with string interpolation.
|
||||
|
|
3
TASKS.md
Normal file
3
TASKS.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
- [ ] Split out tables into an assembly
|
||||
- [ ] Switch to GitVersion for release
|
||||
- [ ] Switch to scripts instead of Node for targets
|
12
flake.lock
12
flake.lock
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1638122382,
|
||||
"narHash": "sha256-sQzZzAbvKEqN9s0bzWuYmRaA03v40gaJ4+iL1LXjaeI=",
|
||||
"lastModified": 1648297722,
|
||||
"narHash": "sha256-W+qlPsiZd8F3XkzXOzAoR+mpFqzm3ekQkJNa+PIh1BQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "74f7e4319258e287b0f9cb95426c9853b282730b",
|
||||
"rev": "0f8662f1319ad6abf89b3380dd2722369fc51ade",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -17,11 +17,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1638198142,
|
||||
"narHash": "sha256-plU9b8r4St6q4U7VHtG9V7oF8k9fIpfXl/KDaZLuY9k=",
|
||||
"lastModified": 1648390671,
|
||||
"narHash": "sha256-u69opCeHUx3CsdIerD0wVSR+DjfDQjnztObqfk9Trqc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8a308775674e178495767df90c419425474582a1",
|
||||
"rev": "ce8cbe3c01fd8ee2de526ccd84bbf9b82397a510",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
let pkgs = nixpkgs.legacyPackages.${system};
|
||||
in {
|
||||
devShell = pkgs.mkShell {
|
||||
buildInputs = [ pkgs.dotnet-sdk_5 pkgs.nodejs-16_x pkgs.nixfmt ];
|
||||
buildInputs = [ pkgs.dotnet-sdk pkgs.nodejs-16_x pkgs.nixfmt ];
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
using System;
|
||||
using System.CommandLine;
|
||||
using System.Linq;
|
||||
|
||||
namespace MfGames.ToolBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Fakes the functionality of the RootCommand because of the given issue:
|
||||
/// https://github.com/dotnet/command-line-api/issues/1471
|
||||
/// In short, RootCommand doesn't resolve properly when working in certain
|
||||
/// situations, mainly having the command line parser in a library that is
|
||||
/// called by another executable.
|
||||
/// </summary>
|
||||
public class FakedRootCommand : Command
|
||||
{
|
||||
public FakedRootCommand(ToolNames names)
|
||||
: base(names.GetExecutableName(), string.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
public static string[] GetArguments()
|
||||
{
|
||||
string[] args = Environment.GetCommandLineArgs();
|
||||
|
||||
return args.Length == 0 ? args : args.Skip(1).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,15 +8,18 @@ using Microsoft.Extensions.Configuration;
|
|||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace MfGames.ToolBuilder.Globals
|
||||
namespace MfGames.ToolBuilder.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// A utility class for handling `--config` options.
|
||||
/// </summary>
|
||||
public class ConfigToolGlobalService
|
||||
public class ConfigToolGlobal
|
||||
{
|
||||
public ConfigToolGlobalService()
|
||||
private readonly string configName;
|
||||
|
||||
public ConfigToolGlobal(string configName)
|
||||
{
|
||||
this.configName = configName;
|
||||
this.ConfigOption = new Option<string[]>(
|
||||
"--config",
|
||||
"Configuration file to use for settings, otherwise a default will be used.")
|
||||
|
@ -46,9 +49,7 @@ namespace MfGames.ToolBuilder.Globals
|
|||
get
|
||||
{
|
||||
// If we don't have an internal name, blow up.
|
||||
string? configName = this.Names?.GetConfigName();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(configName))
|
||||
if (string.IsNullOrWhiteSpace(this.configName))
|
||||
{
|
||||
throw new ApplicationException(
|
||||
"Cannot determine the default configuration path unless the configuration name has been set.");
|
||||
|
@ -61,24 +62,18 @@ namespace MfGames.ToolBuilder.Globals
|
|||
.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
string appDirectory = Path.Combine(
|
||||
configDirectory,
|
||||
configName);
|
||||
this.configName);
|
||||
string configPath = Path.Combine(appDirectory, "Settings.json");
|
||||
|
||||
return configPath;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the application. This is used to figure out
|
||||
/// the name of the configuration file and what is shown on the screen.
|
||||
/// </summary>
|
||||
public ToolNames? Names { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds the common options to the command.
|
||||
/// </summary>
|
||||
/// <param name="root"></param>
|
||||
public void AddOptions(Command root)
|
||||
public void Attach(Command root)
|
||||
{
|
||||
root.AddGlobalOption(this.ConfigOption);
|
||||
}
|
||||
|
@ -107,7 +102,7 @@ namespace MfGames.ToolBuilder.Globals
|
|||
}
|
||||
|
||||
// Otherwise, use the default files.
|
||||
foreach (var config in configs)
|
||||
foreach (string config in configs)
|
||||
{
|
||||
builder.AddJsonFile(config, true, true);
|
||||
}
|
||||
|
@ -128,26 +123,12 @@ namespace MfGames.ToolBuilder.Globals
|
|||
return default;
|
||||
}
|
||||
|
||||
var json = file.ReadAllText();
|
||||
string json = file.ReadAllText();
|
||||
TType result = JsonConvert.DeserializeObject<TType>(json)!;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the internal name of the application, used for the
|
||||
/// configuration path.
|
||||
/// </summary>
|
||||
/// <param name="names">
|
||||
/// The names object for the tool.
|
||||
/// </param>
|
||||
/// <returns>The service for chaining operations.</returns>
|
||||
public ConfigToolGlobalService WithNames(ToolNames? names)
|
||||
{
|
||||
this.Names = names;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the given object to the default configuration file.
|
||||
/// </summary>
|
|
@ -8,20 +8,20 @@ using Serilog.Core;
|
|||
using Serilog.Events;
|
||||
using Serilog.Exceptions;
|
||||
|
||||
namespace MfGames.ToolBuilder.Globals
|
||||
namespace MfGames.ToolBuilder.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// A service for handling logging options.
|
||||
/// </summary>
|
||||
public class LoggingToolGlobalService
|
||||
public class LoggingToolGlobal
|
||||
{
|
||||
public LoggingToolGlobalService()
|
||||
public LoggingToolGlobal()
|
||||
{
|
||||
this.LogLevelOption = new Option<string>(
|
||||
"--log-level",
|
||||
() => nameof(LogEventLevel.Information),
|
||||
string.Format(
|
||||
"Controls the verbosity of the output: {0}. Not case-sensitive and prefixes allowed.",
|
||||
"Controls the verbosity of the output: {0} (defaults to Warning). Not case-sensitive and prefixes allowed.",
|
||||
string.Join(", ", Enum.GetNames<LogEventLevel>())));
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ namespace MfGames.ToolBuilder.Globals
|
|||
/// Adds the common options to the command.
|
||||
/// </summary>
|
||||
/// <param name="root"></param>
|
||||
public void AddOptions(Command root)
|
||||
public void Attach(Command root)
|
||||
{
|
||||
root.AddGlobalOption(this.LogLevelOption);
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ namespace MfGames.ToolBuilder.Globals
|
|||
string level = GlobalOptionHelper.GetArgumentValue(
|
||||
this.LogLevelOption,
|
||||
arguments,
|
||||
"Information");
|
||||
"Warning");
|
||||
LogEventLevel logLevel = level.GetEnumFuzzy<LogEventLevel>(
|
||||
"log level");
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
using System.CommandLine;
|
||||
|
||||
namespace MfGames.ToolBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface that indicates that the given command is a top-level
|
||||
/// command instead of one that is included as a sub-command inside another.
|
||||
/// This is used to arrange the various sub-commands using dependency
|
||||
/// injection and is purely a marker interface.
|
||||
/// </summary>
|
||||
public interface ITopCommand : ICommand
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,43 +1,49 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<Description>A framework for easily creating command line tools using System.CommandLine, Autofac, and Serilog.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- System.CommandLine is pre-release and will be for a while. -->
|
||||
<NoWarn>$(NoWarn);NU5104</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac" Version="6.3.0" />
|
||||
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="7.2.0" />
|
||||
<PackageReference Include="ConsoleTableExt" Version="3.1.9" />
|
||||
<PackageReference Include="CsvHelper" Version="27.2.1" />
|
||||
<PackageReference Include="FluentResults" Version="3.1.0" />
|
||||
<PackageReference Include="FluentResults" Version="3.3.0" />
|
||||
<PackageReference Include="Glob" Version="1.1.9" />
|
||||
<PackageReference Include="Humanizer.Core" Version="2.13.14" />
|
||||
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
||||
<PackageReference Include="MfGames.IO" Version="1.2.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="3.3.0">
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="4.1.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="3.3.0">
|
||||
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="4.1.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="3.3.0">
|
||||
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.1.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||
<PackageReference Include="Serilog.Enrichers.Demystifier" Version="1.0.2" />
|
||||
<PackageReference Include="Serilog.Exceptions" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Exceptions" Version="8.1.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Autofac.DependencyInjection" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="4.2.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta2.21617.1" />
|
||||
<PackageReference Include="System.CommandLine" Version="[2.0.0-beta3.22114.1,)" />
|
||||
<PackageReference Include="System.CommandLine.Hosting" Version="0.4.0-alpha.22114.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
51
src/MfGames.ToolBuilder/ToolBox.cs
Normal file
51
src/MfGames.ToolBuilder/ToolBox.cs
Normal file
|
@ -0,0 +1,51 @@
|
|||
using System;
|
||||
using System.CommandLine.Parsing;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace MfGames.ToolBuilder;
|
||||
|
||||
/// <summary>
|
||||
/// A collection of tools set up for running the command-line shell.
|
||||
/// </summary>
|
||||
public class ToolBox
|
||||
{
|
||||
private readonly string[] arguments;
|
||||
|
||||
private readonly Parser parser;
|
||||
|
||||
public ToolBox(string[] arguments, Parser parser)
|
||||
{
|
||||
this.arguments = arguments;
|
||||
this.parser = parser;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finishes building the tool, parses the arguments, and runs the
|
||||
/// command.
|
||||
/// </summary>
|
||||
/// <returns>An error code, 0 for successful, otherwise false.</returns>
|
||||
public async Task<int> RunAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
ParseResult parseResults = this.parser.Parse(this.arguments);
|
||||
int exitCode = await parseResults.InvokeAsync();
|
||||
|
||||
return exitCode;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.Fatal(
|
||||
exception,
|
||||
"There was a problem running the command: {Arguments}",
|
||||
this.arguments);
|
||||
|
||||
return Environment.ExitCode == 0 ? 1 : Environment.ExitCode;
|
||||
}
|
||||
|
||||
// Get the exit code and return it.
|
||||
// TODO return Environment.ExitCode;
|
||||
}
|
||||
}
|
186
src/MfGames.ToolBuilder/ToolBoxBuilder.cs
Normal file
186
src/MfGames.ToolBuilder/ToolBoxBuilder.cs
Normal file
|
@ -0,0 +1,186 @@
|
|||
using System;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Builder;
|
||||
using System.CommandLine.Hosting;
|
||||
using System.CommandLine.Parsing;
|
||||
using System.IO;
|
||||
|
||||
using Autofac;
|
||||
using Autofac.Extensions.DependencyInjection;
|
||||
|
||||
using MfGames.ToolBuilder.Services;
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace MfGames.ToolBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// A builder pattern for creating a tool box, a nested collection of tools.
|
||||
/// This wraps much of the hosting and command-line parsing infrastructure
|
||||
/// with some opinionated decisions and reduces the amount of boilerplate
|
||||
/// needed to create tools.
|
||||
/// </summary>
|
||||
public class ToolBoxBuilder
|
||||
{
|
||||
private readonly string[] arguments;
|
||||
|
||||
private readonly ConfigToolGlobal config;
|
||||
|
||||
private readonly IHostBuilder hostBuilder;
|
||||
|
||||
private readonly LoggingToolGlobal logging;
|
||||
|
||||
public ToolBoxBuilder(string configName, string[] arguments)
|
||||
{
|
||||
// Create our various services.
|
||||
this.arguments = arguments;
|
||||
this.config = new ConfigToolGlobal(configName);
|
||||
this.logging = new LoggingToolGlobal();
|
||||
|
||||
// Set up logging first so we can report the loading process. This
|
||||
// sets up the Serilog.Log.Logger which means we can use that for
|
||||
// everything beyond this point.
|
||||
this.logging.Configure(arguments);
|
||||
|
||||
// Start up the basic configuration.
|
||||
//
|
||||
// Normally, we would use Host.CreateDefaultBuilder(args) to create
|
||||
// this, but when run as a stand-alone application in someone's
|
||||
// $HOME on Linux, this causes an inotify watcher to be registered
|
||||
// on the entire directory tree (which takes a really long time).
|
||||
//
|
||||
// We also don't need most of the default features.
|
||||
var serviceProviderFactory = new AutofacServiceProviderFactory();
|
||||
this.hostBuilder = new HostBuilder()
|
||||
.UseDefaultServiceProvider(this.ConfigureDefaultServiceProvider)
|
||||
.UseConsoleLifetime()
|
||||
.ConfigureAppConfiguration(this.ConfigureAppConfiguration)
|
||||
.UseSerilog()
|
||||
.UseServiceProviderFactory(serviceProviderFactory)
|
||||
.ConfigureServices(this.ConfigureServices)
|
||||
.ConfigureContainer<ContainerBuilder>(this.ConfigureContainer);
|
||||
}
|
||||
|
||||
public static ToolBoxBuilder Create(
|
||||
string configName,
|
||||
string[] arguments)
|
||||
{
|
||||
return new ToolBoxBuilder(configName, arguments);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a tool box, a collection of tools.
|
||||
/// </summary>
|
||||
/// <returns>The constructed toolbox.</returns>
|
||||
public ToolBox Build()
|
||||
{
|
||||
// There is a Catch-22 with how System.CommandLine is build. It
|
||||
// requires the root command to be passed into the constructor
|
||||
// for the CommandLineBuilder but we can't create the root command
|
||||
// until we build our host.
|
||||
//
|
||||
// To handle this, we build our first host builder (which doesn't
|
||||
// know about the command line parser), use it to get the command
|
||||
// and then pass it into the command-line version to create a
|
||||
// second `HostBuilder` that we don't use.
|
||||
IHost host = this.hostBuilder.Build();
|
||||
ILifetimeScope container = host.Services.GetAutofacRoot()!;
|
||||
|
||||
// Create the root command and attach our globals to that.
|
||||
RootCommand rootCommand = container.Resolve<RootCommand>();
|
||||
|
||||
this.config.Attach(rootCommand);
|
||||
this.logging.Attach(rootCommand);
|
||||
|
||||
// Finish creating the command line builder so we can make the parser.
|
||||
CommandLineBuilder? builder = new CommandLineBuilder(rootCommand)
|
||||
.UseDefaults()
|
||||
.UseHost();
|
||||
|
||||
// Finish building the parser, wrap it in a tool box, and return it.
|
||||
Parser parser = builder.Build();
|
||||
var toolBox = new ToolBox(this.arguments, parser);
|
||||
|
||||
return toolBox;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides additional configuration for Autofac containers.
|
||||
/// </summary>
|
||||
/// <param name="configure">The configuration method.</param>
|
||||
/// <returns>The builder to chain methods.</returns>
|
||||
public ToolBoxBuilder ConfigureContainer(
|
||||
Action<ContainerBuilder> configure)
|
||||
{
|
||||
this.hostBuilder.ConfigureContainer(configure);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides additional configuration for services.
|
||||
/// </summary>
|
||||
/// <param name="configure">The configuration method.</param>
|
||||
/// <returns>The builder to chain methods.</returns>
|
||||
public ToolBoxBuilder ConfigureServices(
|
||||
Action<IServiceCollection> configure)
|
||||
{
|
||||
this.hostBuilder.ConfigureServices(configure);
|
||||
return this;
|
||||
}
|
||||
|
||||
private void ConfigureAppConfiguration(
|
||||
HostBuilderContext context,
|
||||
IConfigurationBuilder builder)
|
||||
{
|
||||
builder.SetBasePath(Directory.GetCurrentDirectory());
|
||||
this.config.Configure(builder, this.arguments);
|
||||
}
|
||||
|
||||
private void ConfigureContainer(
|
||||
HostBuilderContext context,
|
||||
ContainerBuilder builder)
|
||||
{
|
||||
// We want to get logging up and running as soon as possible. We
|
||||
// also hook up the logging to the process exit in an attempt to
|
||||
// make sure the logger is properly flushed before exiting.
|
||||
builder.RegisterInstance(Log.Logger).As<ILogger>().SingleInstance();
|
||||
|
||||
AppDomain.CurrentDomain.ProcessExit +=
|
||||
(_, _) => Log.CloseAndFlush();
|
||||
|
||||
// Register the global services as singletons.
|
||||
builder
|
||||
.RegisterInstance(this.config)
|
||||
.AsSelf()
|
||||
.SingleInstance();
|
||||
builder
|
||||
.RegisterInstance(this.logging)
|
||||
.AsSelf()
|
||||
.SingleInstance();
|
||||
|
||||
// Register the components required to make the CLI work.
|
||||
builder.RegisterModule<ToolBuilderModule>();
|
||||
}
|
||||
|
||||
private void ConfigureDefaultServiceProvider(
|
||||
HostBuilderContext context,
|
||||
ServiceProviderOptions options)
|
||||
{
|
||||
bool isDevelopment = context.HostingEnvironment.IsDevelopment();
|
||||
|
||||
options.ValidateScopes = isDevelopment;
|
||||
options.ValidateOnBuild = isDevelopment;
|
||||
}
|
||||
|
||||
private void ConfigureServices(
|
||||
HostBuilderContext context,
|
||||
IServiceCollection services)
|
||||
{
|
||||
services.AddAutofac();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,174 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Autofac;
|
||||
using Autofac.Extensions.DependencyInjection;
|
||||
|
||||
using MfGames.ToolBuilder.Globals;
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace MfGames.ToolBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// A builder pattern for creating the tool. This wraps much of the hosting
|
||||
/// infrastructure with some opinionated decisions and reduces the amount of
|
||||
/// boilerplate needed to configure the tool.
|
||||
/// </summary>
|
||||
public class ToolBuilder
|
||||
{
|
||||
private readonly string[] arguments;
|
||||
|
||||
private readonly ConfigToolGlobalService configService;
|
||||
|
||||
private readonly IHostBuilder hostBuilder;
|
||||
|
||||
private readonly LoggingToolGlobalService loggingService;
|
||||
|
||||
private readonly ToolNames names;
|
||||
|
||||
public ToolBuilder(
|
||||
ToolNames names,
|
||||
string[] arguments)
|
||||
{
|
||||
// Create our various services.
|
||||
this.names = names;
|
||||
this.arguments = arguments;
|
||||
this.configService = new ConfigToolGlobalService()
|
||||
.WithNames(this.names);
|
||||
this.loggingService = new LoggingToolGlobalService();
|
||||
|
||||
// Set up logging first so we can report the loading process. This
|
||||
// sets up the Serilog.Log.Logger which means we can use that for
|
||||
// everything beyond this point.
|
||||
this.loggingService.Configure(arguments);
|
||||
|
||||
// Start up the basic configuration.
|
||||
//
|
||||
// Normally, we would use Host.CreateDefaultBuilder(args) to create
|
||||
// this, but when run as a stand-alone application in someone's
|
||||
// $HOME on Linux, this causes an inotify watcher to be registered
|
||||
// on the entire directory tree (which takes a really long time).
|
||||
//
|
||||
// We also don't need most of the default features.
|
||||
this.hostBuilder =
|
||||
new HostBuilder()
|
||||
.UseDefaultServiceProvider(
|
||||
(context, options) =>
|
||||
{
|
||||
bool isDevelopment =
|
||||
context.HostingEnvironment.IsDevelopment();
|
||||
options.ValidateScopes = isDevelopment;
|
||||
options.ValidateOnBuild = isDevelopment;
|
||||
})
|
||||
.UseConsoleLifetime()
|
||||
.ConfigureAppConfiguration(this.ConfigureAppConfiguration)
|
||||
.UseSerilog()
|
||||
.UseServiceProviderFactory(
|
||||
new AutofacServiceProviderFactory())
|
||||
.ConfigureServices(this.ConfigureServices)
|
||||
.ConfigureContainer<ContainerBuilder>(
|
||||
this.ConfigureContainer);
|
||||
}
|
||||
|
||||
public static ToolBuilder Create(
|
||||
ToolNames names,
|
||||
string[] arguments)
|
||||
{
|
||||
return new ToolBuilder(names, arguments);
|
||||
}
|
||||
|
||||
public ToolBuilder ConfigureContainer(
|
||||
Action<ContainerBuilder> configure)
|
||||
{
|
||||
this.hostBuilder.ConfigureContainer(configure);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ToolBuilder ConfigureServices(
|
||||
Action<IServiceCollection> configure)
|
||||
{
|
||||
this.hostBuilder.ConfigureServices(configure);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finishes building the tool, parses the arguments, and runs the
|
||||
/// command.
|
||||
/// </summary>
|
||||
/// <returns>An error code, 0 for successful, otherwise false.</returns>
|
||||
public async Task<int> RunAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.hostBuilder
|
||||
.RunConsoleAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.Fatal(
|
||||
exception,
|
||||
"There was a problem running the command: {Arguments}",
|
||||
this.arguments);
|
||||
|
||||
return Environment.ExitCode == 0 ? 1 : Environment.ExitCode;
|
||||
}
|
||||
|
||||
// Get the exit code and return it.
|
||||
return Environment.ExitCode;
|
||||
}
|
||||
|
||||
private void ConfigureAppConfiguration(
|
||||
HostBuilderContext context,
|
||||
IConfigurationBuilder builder)
|
||||
{
|
||||
builder.SetBasePath(Directory.GetCurrentDirectory());
|
||||
this.configService.Configure(builder, this.arguments);
|
||||
}
|
||||
|
||||
private void ConfigureContainer(
|
||||
HostBuilderContext context,
|
||||
ContainerBuilder builder)
|
||||
{
|
||||
// We want to get logging up and running as soon as possible. We
|
||||
// also hook up the logging to the process exit in an attempt to
|
||||
// make sure the logger is properly flushed before exiting.
|
||||
builder.RegisterInstance(Log.Logger).As<ILogger>().SingleInstance();
|
||||
|
||||
AppDomain.CurrentDomain.ProcessExit +=
|
||||
(_, _) => Log.CloseAndFlush();
|
||||
|
||||
// Register the names as a singleton instance.
|
||||
builder.RegisterInstance(this.names)
|
||||
.As<ToolNames>()
|
||||
.SingleInstance();
|
||||
|
||||
// Register the global services as singletons.
|
||||
builder
|
||||
.RegisterInstance(this.configService)
|
||||
.AsSelf()
|
||||
.SingleInstance();
|
||||
builder
|
||||
.RegisterInstance(this.loggingService)
|
||||
.AsSelf()
|
||||
.SingleInstance();
|
||||
|
||||
// Register the components required to make the CLI work.
|
||||
builder.RegisterModule<ToolBuilderModule>();
|
||||
}
|
||||
|
||||
private void ConfigureServices(
|
||||
HostBuilderContext context,
|
||||
IServiceCollection services)
|
||||
{
|
||||
services.AddAutofac();
|
||||
services.AddHostedService<ToolService>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
using Autofac;
|
||||
|
||||
using MfGames.ToolBuilder.Globals;
|
||||
using MfGames.ToolBuilder.Services;
|
||||
|
||||
namespace MfGames.ToolBuilder
|
||||
{
|
||||
|
@ -14,10 +14,8 @@ namespace MfGames.ToolBuilder
|
|||
{
|
||||
builder
|
||||
.RegisterAssemblyTypes(this.GetType().Assembly)
|
||||
.Except<ToolService>()
|
||||
.Except<ToolNames>()
|
||||
.Except<ConfigToolGlobalService>()
|
||||
.Except<LoggingToolGlobalService>()
|
||||
.Except<ConfigToolGlobal>()
|
||||
.Except<LoggingToolGlobal>()
|
||||
.AsSelf()
|
||||
.AsImplementedInterfaces();
|
||||
}
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace MfGames.ToolBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides the name of the tool in its various formats. This is used to
|
||||
/// identify the executable name and other elements used throughout the
|
||||
/// system.
|
||||
/// </summary>
|
||||
public class ToolNames
|
||||
{
|
||||
public ToolNames(string name)
|
||||
{
|
||||
this.Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
|
||||
// The name of the executing program comes in as the first parameter.
|
||||
string[] args = Environment.GetCommandLineArgs();
|
||||
|
||||
if (args.Length > 0)
|
||||
{
|
||||
this.ExecutableName = Path.GetFileNameWithoutExtension(args[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the configuration file. This will fall back to Name
|
||||
/// if not provided.
|
||||
/// </summary>
|
||||
public string? ConfigName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the human-readable name of the application, falling back to Name
|
||||
/// if not provided.
|
||||
/// </summary>
|
||||
public string? DisplayName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the executable. This will fall back to ConfigName
|
||||
/// if this propery is null.
|
||||
/// </summary>
|
||||
public string? ExecutableName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the required name of the application. This can be human-readable
|
||||
/// or not, depending on the developer.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration name using any fallbacks.
|
||||
/// </summary>
|
||||
public string GetConfigName()
|
||||
{
|
||||
return this.ConfigName ?? this.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display name using any fallbacks.
|
||||
/// </summary>
|
||||
public string GetDisplayName()
|
||||
{
|
||||
return this.DisplayName ?? this.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the executable name using any fallbacks.
|
||||
/// </summary>
|
||||
public string GetExecutableName()
|
||||
{
|
||||
return this.ExecutableName ?? this.GetConfigName();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,158 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Builder;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.CommandLine.Parsing;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using MfGames.ToolBuilder.Globals;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
using Serilog;
|
||||
|
||||
#pragma warning disable Serilog004
|
||||
|
||||
// ReSharper disable TemplateIsNotCompileTimeConstantProblem
|
||||
// ReSharper disable SuspiciousTypeConversion.Global
|
||||
|
||||
namespace MfGames.ToolBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements the command-line shell for generating the static site.
|
||||
/// </summary>
|
||||
public class ToolService : IHostedService
|
||||
{
|
||||
private readonly IList<ITopCommand> commands;
|
||||
|
||||
private readonly ConfigToolGlobalService configService;
|
||||
|
||||
private readonly IHostApplicationLifetime lifetime;
|
||||
|
||||
private readonly ILogger logger;
|
||||
|
||||
private readonly LoggingToolGlobalService loggingService;
|
||||
|
||||
private readonly ToolNames names;
|
||||
|
||||
public ToolService(
|
||||
ToolNames names,
|
||||
ILogger logger,
|
||||
IHostApplicationLifetime lifetime,
|
||||
IList<ITopCommand> commands,
|
||||
ConfigToolGlobalService configService,
|
||||
LoggingToolGlobalService loggingService)
|
||||
{
|
||||
this.names = names;
|
||||
this.lifetime = lifetime;
|
||||
this.commands = commands;
|
||||
this.configService = configService;
|
||||
this.loggingService = loggingService;
|
||||
this.logger = logger.ForContext<ToolService>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
this.lifetime.ApplicationStarted
|
||||
.Register(
|
||||
() =>
|
||||
{
|
||||
Task.Run(
|
||||
async () =>
|
||||
await this.RunAsync().ConfigureAwait(false),
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private FakedRootCommand CreateRootCommand()
|
||||
{
|
||||
// Create the root command and add in the top-level commands
|
||||
// underneath it. We can't use the "real" `RootCommand` here because
|
||||
// it doesn't work in stripped executables (because this is a
|
||||
// library) so we fake it with a "normal" command.
|
||||
var root = new FakedRootCommand(this.names);
|
||||
|
||||
foreach (var command in this.commands)
|
||||
{
|
||||
root.AddCommand((Command)command);
|
||||
}
|
||||
|
||||
// Add the universal options.
|
||||
this.loggingService.AddOptions(root);
|
||||
this.configService.AddOptions(root);
|
||||
|
||||
// Return the resulting container.
|
||||
return root;
|
||||
}
|
||||
|
||||
private void OnException(Exception exception, InvocationContext context)
|
||||
{
|
||||
if (exception is ToolException toolException)
|
||||
{
|
||||
this.logger.Fatal(toolException.Message);
|
||||
|
||||
foreach (var message in toolException.Messages)
|
||||
{
|
||||
this.logger.Fatal(message);
|
||||
}
|
||||
|
||||
Environment.ExitCode = toolException.ExitCode;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.logger.Fatal(
|
||||
exception,
|
||||
"Unhandled exception!");
|
||||
|
||||
Environment.ExitCode = Environment.ExitCode == 0
|
||||
? 1
|
||||
: Environment.ExitCode;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Build the command tree.
|
||||
FakedRootCommand root = this.CreateRootCommand();
|
||||
string[] args = FakedRootCommand.GetArguments();
|
||||
|
||||
// Execute the command.
|
||||
this.logger.Verbose(
|
||||
"Running the command-line arguments: {Arguments}",
|
||||
args);
|
||||
|
||||
CommandLineBuilder builder = new CommandLineBuilder(root)
|
||||
.UseDefaults()
|
||||
.UseExceptionHandler(this.OnException);
|
||||
|
||||
Parser cli = builder.Build();
|
||||
int exitCode = await cli
|
||||
.InvokeAsync(args)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (exitCode != 0)
|
||||
{
|
||||
Environment.ExitCode = exitCode;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Stop the application once the work is done.
|
||||
this.lifetime.StopApplication();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +1,29 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CliWrap" Version="3.3.3" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.0">
|
||||
<PackageReference Include="CliWrap" Version="3.4.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="JunitXml.TestLogger" Version="3.0.110" />
|
||||
<PackageReference Include="MfGames.IO" Version="1.2.3" />
|
||||
<PackageReference Include="MfGames.TestSetup" Version="1.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="3.3.0">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="4.1.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="3.3.0">
|
||||
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="4.1.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="3.3.0">
|
||||
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.1.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
|
|
@ -7,12 +7,12 @@ using MfGames.ToolBuilder;
|
|||
|
||||
namespace SampleTool
|
||||
{
|
||||
public class CrashTopCommand : Command, ITopCommand, ICommandHandler
|
||||
public class CrashCommand : Command, ICommandHandler
|
||||
{
|
||||
private readonly Option<bool> messyOption;
|
||||
|
||||
/// <inheritdoc />
|
||||
public CrashTopCommand()
|
||||
public CrashCommand()
|
||||
: base("crash", "Crash the application with an exception.")
|
||||
{
|
||||
this.Handler = this;
|
67
tests/SampleTool/LogCommand.cs
Normal file
67
tests/SampleTool/LogCommand.cs
Normal file
|
@ -0,0 +1,67 @@
|
|||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace SampleTool;
|
||||
|
||||
public class LogCommand : Command, ICommandHandler
|
||||
{
|
||||
private readonly ILogger<LogCommand> extensionLogger;
|
||||
|
||||
private readonly ILogger serilogLogger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public LogCommand(
|
||||
ILoggerFactory loggerFactory,
|
||||
ILogger serilogLogger)
|
||||
: base("log", "Shows various logging messages.")
|
||||
{
|
||||
this.serilogLogger = serilogLogger;
|
||||
this.extensionLogger = loggerFactory.CreateLogger<LogCommand>();
|
||||
this.Handler = this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> InvokeAsync(InvocationContext context)
|
||||
{
|
||||
// Show the serilog logging.
|
||||
this.serilogLogger.Debug("Serilog Debug");
|
||||
this.serilogLogger.Error("Serilog Error");
|
||||
this.serilogLogger.Fatal("Serilog Fatal");
|
||||
this.serilogLogger.Information("Serilog Information");
|
||||
this.serilogLogger.Verbose("Serilog Verbose");
|
||||
this.serilogLogger.Warning("Serilog Warning");
|
||||
|
||||
// Show the extension logging.
|
||||
this.extensionLogger.LogCritical(
|
||||
"System.Extension.Logging LogCritical");
|
||||
this.extensionLogger.LogDebug(
|
||||
"System.Extension.Logging LogDebug");
|
||||
this.extensionLogger.LogError(
|
||||
"System.Extension.Logging LogError");
|
||||
this.extensionLogger.LogInformation(
|
||||
"System.Extension.Logging LogInformation");
|
||||
this.extensionLogger.LogTrace(
|
||||
"System.Extension.Logging LogTrace");
|
||||
this.extensionLogger.LogWarning(
|
||||
"System.Extension.Logging LogWarning");
|
||||
|
||||
// Show Serilog working through logging extensions.
|
||||
var hash = new { Number = 1, String = "String" };
|
||||
|
||||
this.extensionLogger.LogInformation(
|
||||
"Contextual information via {Name} and {Quotes:l}",
|
||||
"extension logger",
|
||||
"without quotes");
|
||||
this.extensionLogger.LogInformation(
|
||||
"Contextual information via {@Nested}",
|
||||
hash);
|
||||
|
||||
// We're good.
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
|
@ -10,11 +10,10 @@ namespace SampleTool
|
|||
{
|
||||
public static async Task<int> Main(string[] args)
|
||||
{
|
||||
return await ToolBuilder
|
||||
.Create(
|
||||
new ToolNames("SampleApplication"),
|
||||
args)
|
||||
return await ToolBoxBuilder
|
||||
.Create("SampleTool", args)
|
||||
.ConfigureContainer(ConfigureContainer)
|
||||
.Build()
|
||||
.RunAsync();
|
||||
}
|
||||
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta2.21617.1" />
|
||||
<PackageReference Include="System.CommandLine" Version="[2.0.0-beta3.22114.1,)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
using Autofac;
|
||||
using System.CommandLine;
|
||||
|
||||
using Autofac;
|
||||
|
||||
using SampleTool;
|
||||
|
||||
namespace MfGames.ToolBuilder
|
||||
{
|
||||
|
@ -14,6 +18,25 @@ namespace MfGames.ToolBuilder
|
|||
.RegisterAssemblyTypes(this.GetType().Assembly)
|
||||
.AsSelf()
|
||||
.AsImplementedInterfaces();
|
||||
|
||||
builder
|
||||
.Register(
|
||||
c =>
|
||||
{
|
||||
var root = new RootCommand
|
||||
{
|
||||
Name = "sample-tool",
|
||||
Description =
|
||||
"A sample tool that demonstrates functionality",
|
||||
};
|
||||
|
||||
root.AddCommand(c.Resolve<CrashCommand>());
|
||||
root.AddCommand(c.Resolve<TableCommand>());
|
||||
root.AddCommand(c.Resolve<LogCommand>());
|
||||
|
||||
return root;
|
||||
})
|
||||
.AsSelf();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,23 +2,21 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.CommandLine.IO;
|
||||
using System.Data;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using MfGames.ToolBuilder;
|
||||
using MfGames.ToolBuilder.Tables;
|
||||
|
||||
namespace SampleTool
|
||||
{
|
||||
public class TableTopCommand : Command, ITopCommand, ICommandHandler
|
||||
public class TableCommand : Command, ICommandHandler
|
||||
{
|
||||
private readonly DataTable table;
|
||||
|
||||
private readonly TableToolService tableService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public TableTopCommand(TableToolService.Factory tableService)
|
||||
public TableCommand(TableToolService.Factory tableService)
|
||||
: base("table", "Display a table.")
|
||||
{
|
||||
// Create the table structure.
|
||||
|
@ -36,7 +34,7 @@ namespace SampleTool
|
|||
"DefaultString",
|
||||
"DefaultInt32",
|
||||
});
|
||||
|
||||
|
||||
// This class handles the command.
|
||||
this.Handler = this;
|
||||
}
|
Reference in a new issue