python 控制台框架_为什么以及如何创建另一个控制台框架

python 控制台框架

介绍 (Introduction)

Console applications are becoming viral tools for developers. After a trend that was going to move each non-visual tool to GUIs for better control, in the last years, we see the success of CLIs. Just remind to Docker and Kubernetes. In the past months, I developed an open-source headless CMS called RawCMS, and I put a CLI application on it. I started this tool by using the library in the .NET market, and the final experience wasn’t so good. I completed the task, but the next time I had to develop a client application, I decided to invest some time trying to do something better. I finished developing a straightforward console framework that automates command execution and the interaction with the user.

控制台应用程序正成为开发人员的病毒式工具。 趋势是将每个非可视工具都移至GUI以实现更好的控制,在过去的几年中,我们看到了CLI的成功。 只要提醒一下Docker和Kubernetes。 在过去的几个月中,我开发了一个名为RawCMS的开源无头CMS,并在上面放置了CLI应用程序。 我通过使用.NET市场中的库来启动此工具,但最终的体验不是很好。 我完成了任务,但是下次必须开发客户端应用程序时,我决定花一些时间尝试做一些更好的事情。 我完成了一个简单的控制台框架的开发,该框架可自动执行命令并与用户进行交互。

In this article, I will explain how I did it — focus on the exciting part I learned. The code is available on GitHub, as usual, and I released a Nuget package for testing (links at the end of the article).

在本文中,我将解释我是如何做到的-着重于我学到的令人兴奋的部分。 像往常一样,该代码可在GitHub上获得,并且我发布了一个Nuget包进行测试(本文结尾的链接)。

为什么要使用其他控制台框架 (Why Another Console Framework)

The honest answer is because I needed to find an excuse for testing Github’s actions. I could have done a dummy project for this, but I do not like to work with fake thing too much. That’s why I take my last poor experience on .NET RawCMS CLI and try to do something useful. Now, I’ll try to find a rational reason.

诚实的答案是因为我需要找到测试Github动作的借口。 我本可以为此做一个虚拟项目,但我不喜欢过多地处理假货。 这就是为什么我会在.NET RawCMS CLI上经历最后的糟糕经验,然后尝试做一些有用的事情。 现在,我将尝试找到一个合理的原因。

The framework I used until now, Commandlineparser, works properly, and it is well structured. It has this simple approach:

到目前为止,我使用的框架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!");
           }
       });
    }
}

This lead in organizing the code as follows:

这导致组织代码如下:

  • You create a class per command that map inputs

    您为每个映射输入的命令创建一个类
  • In this class, you will define annotation on each field that will be parsed from the command line input

    在此类中,您将在将从命令行输入中解析的每个字段上定义注释
  • You will define a piece of code that will receive the class input

    您将定义一段代码,该代码将接收类输入

I found this approach very well structured, with a good definition of model and business logic. Anyway, for a simple project, ask you to produce many classes, keep them synced, and this is a mess of time for very simple programs. Moreover, it doesn’t provide any support for working in interactive mode or automating scripts.

我发现这种方法的结构很好,对模型和业务逻辑的定义很好。 无论如何,对于一个简单的项目,要求您生成许多类,并使它们保持同步,这对于非常简单的程序来说是一堆时间。 而且,它不提供以交互方式工作或自动化脚本的任何支持。

To overcome these limitations, I decided to use a different approach:

为了克服这些限制,我决定使用其他方法:

  1. The parameter and command definition will be derived automatically from the method annotations.

    参数和命令定义将从方法注释中自动得出。
  2. I will implement an architecture that will allow the user to call a command directly or simply define a script and run it, utilizing the command inside it.

    我将实现一个体系结构,该体系结构将允许用户直接调用命令或简单地定义脚本并利用其中的命令运行它。

If what I tried to explain is not clear, maybe we need an example.

如果我尝试解释的内容不清楚,也许我们需要一个例子。

class Program
{
    static void Main(string[] args)
    {
        ConsoleAuto.Config(args)
            .LoadCommands()
            .Run();
    }

    [ConsoleCommand]
    public void MyMethod(string inputOne, int inputTwo)
    {
        //do stuff here
    }
}

and you can use it like:

您可以像这样使用它:

> 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

I hope now it is a little bit clear.

我希望现在有点清楚了。

这个怎么运作 (How It Works)

This tool is very simple. It can be decomposed in a few subparts, form top to bottom:

这个工具很简单。 它可以分解为几个小部分,从上到下形成:

  1. the fluent registration class

    流利的注册课程
  2. the program definition (sequence of command to be executed)

    程序定义(要执行的命令序列)
  3. the command implementation

    命令执行
  4. the base execution

    基本执行

流利的注册班 (The Fluent Registration Class)

This class is designed to offer a simple way to be integrated into console applications. You will need something like this:

此类旨在提供一种集成到控制台应用程序中的简单方法。 您将需要以下内容:

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();

程序定义(要执行的命令顺序) (The Program Definition (Sequence of Command to Be Executed))

The console parsing ends in the hydration of a class that represents the program definition. This class contains the list of commands to be executed and the argument that they need to work. All the things that will be executed at runtime pass from these commands. For example, the welcome message is a command that is executed at each time, or there is an “info” command that displays all available options.

控制台解析结束于表示程序定义的类的混合。 此类包含要执行的命令的列表以及它们需要工作的参数。 在运行时将执行的所有操作均从这些命令传递。 例如,欢迎消息是每次执行的命令,或者有一个“ info”命令显示所有可用选项。

This is the program definition:

这是程序定义:

public class ProgramDefinition
{
    public Dictionary<string, object> State { get; set; }

    public Dictionary<string, CommandDefinition> Commands { get; set; }
}

That is loaded from a YAML script file or by command-line invocation.

它是从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

命令执行 (The Command Implementation)

The command implementation represents a command that can be invoked. I created two annotations, one for elevating a regular method as a command, and one for describing the parameter.

该命令实现表示可以调用的命令。 我创建了两个注释,一个用于将常规方法提升为命令,另一个用于描述参数。

The implementation is something like:

实现类似于:

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; } 
}

The ConsoleAuto scans all assembly to find all commands and adds it to its internal set of available commands.

ConsoleAuto扫描所有程序集以查找所有命令,并将其添加到其内部可用命令集中。

基本执行 (The Base Execution)

When the user asks for command execution, the class is activated by .NET Core DI and the method executed using reflection.

当用户要求执行命令时,该类由.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设置 (The DevOps Setup)

After spending a couple of hours in this funny library, I finally arrived at the point where I can start testing the GitHub actions.

在这个有趣的库中花了几个小时之后,我终于到达可以开始测试GitHub动作的地步了。

What I wanted to set up was a simple flow that:

我要设置的是一个简单的流程,该流程为:

  1. builds the code

    建立代码
  2. tests the code

    测试代码
  3. packages the code into a Nuget package

    将代码打包到Nuget包中
  4. pushes the package to NuGet

    将包裹推到NuGet

The GitHub setup was easy, and you just need to create a YAML file into the .github/workflows folder.

GitHub设置很容易,您只需要在.github / workflows文件夹中创建一个YAML文件。

The file I used is this one.

我使用的文件就是这个。

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

Please note the automatic versioning based on the progressive build number.

请注意基于渐进版本号的自动版本控制。

The steps are:

这些步骤是:

  1. patching the versions

    修补版本
  2. build

    建立
  3. test

    测试
  4. publish to NuGet

    发布到NuGet

The paths are easily done replacing the version in .csproj file. It is done as the first step so that all following compilation is made with the right build number.

这些路径很容易替换掉.csproj文件中的版本。 第一步是完成此操作,以便使用正确的内部版本号进行所有后续编译。

sed -i "s/1.0.0/1.0.$GITHUB_RUN_NUMBER.0/g" ConsoleAuto/ConsoleAuto.csproj

The script assumes that in csproj, there is always version tag 1.0.0. You can use regexp to improve that solution.

该脚本假定在csproj中始终有版本标记1.0.0。 您可以使用regexp改进该解决方案。

The build\test commands are quite standard and are equivalent to:

build \ test命令非常标准,等效于:

dotnet restore
dotnet build --configuration Release --no-restore
dotnet test --no-restore --verbosity normal

The final publishing step is a third party addon that runs internally the pack command and then the push one. If you look to the GitHub history, you will find something similar to this:

最后的发布步骤是在内部运行pack命令,然后再推送一个的第三方插件。 如果查看GitHub历史记录,将会发现类似以下内容:

dotnet pack  --no-build -c Release ConsoleAuto/ConsoleAuto.csproj 
dotnet nuget push *.nupkg 

我从这次经历中学到了什么 (What I Learned From this Experience)

In our field, all we need is already discovered or done. The fast way to complete a task is by doing it using what you find. Never reinvent the wheel. Anyway, when you need to learn something new, you may be encouraged by trying to create something useful. This can make your learning more practical and let you clash with real problems.

在我们的领域中,我们所需的一切已经被发现或完成。 完成任务的快速方法是使用发现的内容完成任务。 永远不要重新发明轮子。 无论如何,当您需要学习新知识时,可以尝试创建有用的东西来鼓励您。 这可以使您的学习更加实用,并使您与实际问题发生冲突。

After a Saturday spent on this library, I learned:

在这个图书馆度过了一个星期六之后,我了解到:

  • There was already a library that maps automatically form methods to command line arguments. This library is very well done and also has a web interface for running commands visually using swagger UI.

    已经有一个库,可以自动将表单方法映射到命令行参数。 该库做得非常好,还具有一个Web界面,可使用swagger UI直观地运行命令。
  • The GitHub actions are very cool, but the problem is on third party plugins that are not well documented or fully working. Just give them some time to mature. But for this topic, I will need another article. 😃

    GitHub的动作非常酷,但是问题出在没有充分说明或无法正常工作的第三方插件上。 给他们一点时间成熟。 但是对于这个话题,我将需要另一篇文章。 😃

参考文献 (References)

翻译自: https://www.codeproject.com/Articles/5271596/Why-and-How-I-Created-Another-Console-Framework

python 控制台框架

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值