我为何以及如何创建另一个控制台框架





5.00/5 (11投票s)
有时,为了学习,我们需要找借口来开始新项目
引言
控制台应用程序正成为开发人员的流行工具。继将每个非可视化工具移至 GUI 以获得更好控制的趋势之后,近年来,我们看到了 CLI 的成功。只需回想一下 Docker 和 Kubernetes。在过去的几个月里,我开发了一个名为 RawCMS 的开源无头 CMS,并在其中放置了一个 CLI 应用程序。我开始使用 .NET 市场上的库来开发这个工具,但最终的体验并不好。我完成了任务,但下次我必须开发客户端应用程序时,我决定花些时间尝试做些更好的事情。我完成了一个简单的控制台框架,该框架可自动执行命令执行和与用户的交互。
在本文中,我将解释我是如何做到的——重点介绍我学到的激动人心的部分。代码一如既往地可以在 GitHub 上找到,并且我发布了一个 Nuget 包供测试(链接在文章末尾)。
为什么要有另一个控制台框架
老实说,是因为我需要找个借口来测试 GitHub Actions。我本可以做一个虚拟项目来完成这个,但我不太喜欢处理虚假的东西。这就是为什么我借鉴了我在 .NET RawCMS CLI 上糟糕的经历,并尝试做一些有用的事情。现在,我将尝试找一个合理的理由。
我至今为止使用的框架 `Commandlineparser` 工作正常,并且结构良好。它有这个简单的做法
class Program
{
public class Options
{
[Option('v', "verbose", Required = false, HelpText = "Set output to verbose messages.")]
public bool Verbose { get; set; }
}
static void Main(string[] args)
{
Parser.Default.ParseArguments<Options>(args)
.WithParsed<Options>(o =>
{
if (o.Verbose)
{
Console.WriteLine($"Verbose output enabled. Current Arguments: -v {o.Verbose}");
Console.WriteLine("Quick Start Example! App is in Verbose mode!");
}
else
{
Console.WriteLine($"Current Arguments: -v {o.Verbose}");
Console.WriteLine("Quick Start Example!");
}
});
}
}
这导致代码组织如下
- 你为映射输入的每个命令创建一个类
- 在此类中,你将为从命令行输入解析的每个字段定义注解
- 你将定义一段代码来接收类输入
我认为这种方法结构非常好,对模型和业务逻辑有很好的定义。但是,对于一个简单的项目,它要求你生成许多类,并保持它们的同步,这对于非常简单的程序来说是浪费时间。此外,它不提供任何支持来在交互模式下工作或自动化脚本。
为了克服这些限制,我决定采用不同的方法
- 参数和命令定义将从方法注解中自动派生。
- 我将实现一个允许用户直接调用命令或简单定义一个脚本并运行它的架构,利用其中的命令。
如果我试图解释的内容不清楚,也许我们需要一个例子。
class Program
{
static void Main(string[] args)
{
ConsoleAuto.Config(args)
.LoadCommands()
.Run();
}
[ConsoleCommand]
public void MyMethod(string inputOne, int inputTwo)
{
//do stuff here
}
}
你可以这样使用它
> MyProgram.exec CommandName --arg value --arg2 value
> MyProgram.exec Exec MySettings.yaml # (settings from file input!)
> MyProgram.execWelcome to MyProgram!
This is the commands available, digit the number to get more info
0 - info: Display this text
1 - MyMethod: Execute a test method
我希望现在它会稍微清晰一些。
工作原理
这个工具非常简单。它可以从上到下分解为几个子部分
- 流畅的注册类
- 程序定义(要执行的命令序列)
- 命令实现
- 基本执行
流畅的注册类
这个类旨在为集成到控制台应用程序中提供一种简单的方式。你需要类似这样的东西
ConsoleAuto.Config(args, servicecollection)// pass servicecollection to use
// the same container of main application
.LoadCommands() // Load all commands from entry assembly +
// base commands
.LoadCommands(assembly) // Load from a custom command
.LoadFromType(typeof(MyCommand)) // Load a single command
.Register<MyService>() // add a service di di container used in my commands
.Register<IMyService2>(new Service2()) // add a service di di container used
// in my commands, with a custom implementation
.Default("MyDefaultCommand") // specify the default action if app starts
// without any command
.Configure(config => {
//hack the config here
})
.Run();
程序定义(要执行的命令序列)
控制台解析最终会实例化一个表示程序定义的类。该类包含要执行的命令列表以及它们工作所需的参数。所有将在运行时执行的内容都通过这些命令传递。例如,欢迎消息是一个每次都执行的命令,或者有一个“info”命令可以显示所有可用的选项。
这是程序定义
public class ProgramDefinition
{
public Dictionary<string, object> State { get; set; }
public Dictionary<string, CommandDefinition> Commands { get; set; }
}
它从 YAML 脚本文件加载,或通过命令行调用。
Commands:
welcome_step1:
Action: welcome
Desctiption:
Args:
header: my text (first line)
welcome_step2:
Action: welcome
Desctiption:
Args:
header: my text (second line)
main_step:
Action: CommandOne
Description:
Args:
text: I'm the central command output!
State:
text: myglobal
命令实现
命令实现代表一个可以被调用的命令。我创建了两个注解,一个用于将常规方法提升为命令,另一个用于描述参数。
实现有点像
public class CommandImplementation
{
public string Name { get; set; }
public MethodInfo Method { get; set; }
public Dictionary<string, object> DefaultArgs { get; set; }
public ExecutionMode Mode { get; set; } = ExecutionMode.OnDemand;
public int Order { get; set; }
public bool IsPublic { get; set; } = true;
public string Info { get; set; }
public List<ParamImplementation> Params { get; set; }
}
ConsoleAuto 会扫描所有程序集以查找所有命令,并将其添加到其内部的可用命令集中。
基本执行
当用户请求执行命令时,该类通过 .NET Core DI 激活,并通过反射执行方法。
public void Run()
{
LoadCommands(this.GetType().Assembly);//load all system commands
//let DI resolve the type for you
this.serviceBuilder.AddSingleton<ConsoleAutoConfig>(this.config);
foreach (var command in this.config.programDefinition.Commands)
{
var commandDef = command.Value;
var commandImpl = this.config
.AvailableCommands
.FirstOrDefault(x => x.Name == commandDef.Action);
InvokeCommand(commandImpl, commandDef);
}
}
private void InvokeCommand
(CommandImplementation commandImpl, CommandDefinition commandDefinition)
{
//boring code that get arguments value from command definition
commandImpl.Method.Invoke(instance, args.ToArray());
}
DevOps 设置
在花费了几个小时研究这个有趣的库之后,我终于可以开始测试 GitHub Actions 了。
我想设置一个简单的流程,该流程
- 构建代码
- 测试代码
- 将代码打包成 Nuget 包
- 将包推送到 NuGet
GitHub 设置很简单,你只需要在 `.github/workflows` 文件夹中创建一个 YAML 文件。
我使用的文件是这个。
name: .NET Core
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Print version
run: sed -i "s/1.0.0/1.0.$GITHUB_RUN_NUMBER.0/g" ConsoleAuto/ConsoleAuto.csproj
&& cat ConsoleAuto/ConsoleAuto.csproj
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 3.1.101
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --no-restore --verbosity normal
- name: publish on version change
id: publish_nuget
uses: rohith/publish-nuget@v2
with:
# Filepath of the project to be packaged, relative to root of repository
PROJECT_FILE_PATH: ConsoleAuto/ConsoleAuto.csproj
# NuGet package id, used for version detection & defaults to project name
PACKAGE_NAME: ConsoleAuto
# Filepath with version info, relative to root of repository
# & defaults to PROJECT_FILE_PATH
VERSION_FILE_PATH: ConsoleAuto/ConsoleAuto.csproj
# Regex pattern to extract version info in a capturing group
VERSION_REGEX: <Version>(.*)<\/Version>
# Useful with external providers like Nerdbank.GitVersioning,
# ignores VERSION_FILE_PATH & VERSION_REGEX
# VERSION_STATIC: ${{steps.version.outputs.Version }}
# Flag to toggle git tagging, enabled by default
TAG_COMMIT: true
# Format of the git tag, [*] gets replaced with actual version
# TAG_FORMAT: v*
# API key to authenticate with NuGet server
NUGET_KEY: ${{secrets.NUGET_API_KEY}}
# NuGet server uri hosting the packages, defaults to https://api.nuget.org
# NUGET_SOURCE: https://api.nuget.org
# Flag to toggle pushing symbols along with nuget package to the server,
# disabled by default
# INCLUDE_SYMBOLS: false
请注意,版本是基于累积的构建号自动生成的。
步骤是
- 修补版本
- 构建
- 测试
- 发布到 NuGet
这些路径可以通过替换 `.csproj` 文件中的版本轻松完成。它被作为第一步完成,以便所有后续的编译都使用正确的构建号。
sed -i "s/1.0.0/1.0.$GITHUB_RUN_NUMBER.0/g" ConsoleAuto/ConsoleAuto.csproj
该脚本假设 csproj 文件中始终有一个版本标签 1.0.0。你可以使用正则表达式来改进此解决方案。
`build` 和 `test` 命令是相当标准的,等同于
dotnet restore
dotnet build --configuration Release --no-restore
dotnet test --no-restore --verbosity normal
最后的发布步骤是一个第三方插件,它在内部运行 `pack` 命令,然后是 `push` 命令。如果你查看 GitHub 的历史记录,你会发现类似这样的内容
dotnet pack --no-build -c Release ConsoleAuto/ConsoleAuto.csproj
dotnet nuget push *.nupkg
我从这次经历中学到了什么
在我们的领域,我们所需要的一切都已经被发现或完成了。完成任务的快速方法是使用你找到的工具。永远不要重复造轮子。无论如何,当你需要学习新东西时,你可以通过尝试创造有用的东西来获得鼓励。这可以使你的学习更具实践性,并让你面对真正的问题。
在花了一个周六研究这个库之后,我学到了
- 已经有一个库可以自动从方法映射到命令行参数。这个库做得非常好,还有一个 Web 界面可以使用 swagger UI 可视化地运行命令。
- GitHub Actions 非常棒,但问题在于第三方插件,这些插件的文档不全或不完全可用。给它们一些时间来成熟。但关于这个话题,我需要另一篇文章。😃
参考文献
- ConsoleAuto 项目在 GitHub 上
- RAWCMS ——提到的无头 CMS
- 文章中提到的库是 Commandlineparser 和 ConsoleAppFramework,我建议将它们作为首选进行测试。
历史
- 2020 年 7 月 3 日:初始版本