热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

使用.NetCore编写命令行工具(CLI)

命令行工具(CLI)命令行工具(CLI)是在图形用户界面得到普及之前使用最为广泛的用户界面,它通常不支持鼠标,用户通过键盘输入指令,计算机接收到指令后,予以执行。通常认为,命令行工

命令行工具(CLI)

  命令行工具(CLI)是在图形用户界面得到普及之前使用最为广泛的用户界面,它通常不支持鼠标,用户通过键盘输入指令,计算机接收到指令后,予以执行。

  通常认为,命令行工具(CLI)没有图形用户界面(GUI)那么方便用户操作。因为,命令行工具的软件通常需要用户记忆操作的命令,但是,由于其本身的特点,命令行工具要较图形用户界面节约计算机系统的资源。在熟记命令的前提下,使用命令行工具往往要较使用图形用户界面的操作速度要快。所以,图形用户界面的操作系统中,都保留着可选的命令行工具。

  另外,命令行工具(CLI)应该是一个开箱即用的工具,不需要安装任何依赖。

  一些熟悉的CLI工具如下:

  1. dotnet cli

  2. vue cli

  3. angular cli

  4. aws cli

  5. azure cli

 

 指令设计

  本文将使用.Net Core(版本3.1.102)编写一个CLI工具,实现配置管理以及条目(item)管理(调用WebApi实现),详情如下:

  

 

框架说明 

  编写CLI使用的主要框架是CommandLineUtils,它主要有以下优势:

  1. 良好的语法设计

  2. 支持依赖注入

  3. 支持generic host

 

WebApi

  提供api让cli调用,实现条目(item)的增删改查:

[Route("api/items")]
[ApiController]
public class ItemsController : ControllerBase
{
    private readonly IMemoryCache _cache;
    private readonly string _key = "items";

    public ItemsController(IMemoryCache memoryCache)
    {
        _cache = memoryCache;
    }

    [HttpGet]
    public IActionResult List()
    {
        var items = _cache.Get>(_key);
        return Ok(items);
    }

    [HttpGet("{id}")]
    public IActionResult Get(string id)
    {
        var item = _cache.Get>(_key).FirstOrDefault(n => n.Id == id);
        return Ok(item);
    }

    [HttpPost]
    public IActionResult Create(ItemForm form)
    {
        var items = _cache.Get>(_key) ?? new List();

        var item = new Item
        {
            Id = Guid.NewGuid().ToString("N"),
            Name = form.Name,
            Age = form.Age
        };

        items.Add(item);

        _cache.Set(_key, items);
        
        return Ok(item);
    }

    [HttpDelete("{id}")]
    public IActionResult Delete(string id)
    {
        var items = _cache.Get>(_key);

        var item = items?.SingleOrDefault(n => n.Id == id);
        if (item == null)
        {
            return NotFound();
        }

        items.Remove(item);
        _cache.Set(_key, items);

        return Ok();
    }
}

 

CLI

  1. Program - 函数入口

[HelpOption(Inherited = true)] //显示指令帮助,并且让子指令也继承此设置
[Command(Description = "A tool to communicate with web api"), //指令描述
 Subcommand(typeof(ConfigCommand), typeof(ItemCommand))] //子指令
class Program
{
    public static int Main(string[] args)
    {
//配置依赖注入
var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(PhysicalConsole.Singleton); serviceCollection.AddSingleton(); serviceCollection.AddHttpClient(); var services = serviceCollection.BuildServiceProvider(); var app = new CommandLineApplication(); app.Conventions .UseDefaultConventions() .UseConstructorInjection(services); var cOnsole= (IConsole)services.GetService(typeof(IConsole)); try { return app.Execute(args); } catch (UnrecognizedCommandParsingException ex) //处理未定义指令 { console.WriteLine(ex.Message); return -1; } }
//指令逻辑
private int OnExecute(CommandLineApplication app, IConsole console) { console.WriteLine("Please specify a command."); app.ShowHelp(); return 1; } }

 

  2. ConfigCommand和ItemCommand - 实现的功能比较简单,主要是指令描述以及指定对应的子指令

[Command("config", Description = "Manage config"),
 Subcommand(typeof(GetCommand), typeof(SetCommand))]
public class ConfigCommand
{
    private int OnExecute(CommandLineApplication app, IConsole console)
    {
        console.Error.WriteLine("Please submit a sub command.");
        app.ShowHelp();
        return 1;
    }
}

[Command("item", Description = "Manage item"),
 Subcommand(typeof(CreateCommand), typeof(GetCommand), typeof(ListCommand), typeof(DeleteCommand))]
public class ItemCommand
{
    private int OnExecute(CommandLineApplication app, IConsole console)
    {
        console.Error.WriteLine("Please submit a sub command.");
        app.ShowHelp();
        return 1;
    }
}

 

  3. ConfigService - 配置管理的具体实现,主要是文件读写

public interface IConfigService
{
    void Set();

    Config Get();
}

public class ConfigService: IConfigService
{
    private readonly IConsole _console;
    private readonly string _directoryName;
    private readonly string _fileName;

    public ConfigService(IConsole console)
    {
        _console = console;
        _directoryName = ".api-cli";
        _fileName = "config.json";
    }

    public void Set()
    {
        var directory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), _directoryName);
        if (!Directory.Exists(directory))
        {
            Directory.CreateDirectory(directory);
        }

        var cOnfig= new Config
        {
//弹出交互框,让用户输入,设置默认值为http://localhost:5000/ Endpoint
= Prompt.GetString("Specify the endpoint:", "http://localhost:5000/") }; if (!config.Endpoint.EndsWith("/")) { config.Endpoint += "/"; } var filePath = Path.Combine(directory, _fileName); using (var outputFile = new StreamWriter(filePath, false, Encoding.UTF8)) { outputFile.WriteLine(JsonConvert.SerializeObject(config, Formatting.Indented)); } _console.WriteLine($"Config saved in {filePath}."); } public Config Get() { var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), _directoryName, _fileName); if (File.Exists(filePath)) { var cOntent= File.ReadAllText(filePath); try { var cOnfig= JsonConvert.DeserializeObject(content); return config; } catch { _console.WriteLine("The config is invalid, please use 'config set' command to reset one."); } } else { _console.WriteLine("Config is not existed, please use 'config set' command to set one."); } return null; } }

 

  4. ItemClient - 调用Web Api的具体实现,使用HttpClientFactory的方式

public interface IItemClient
{
    Task<string> Create(ItemForm form);

    Task<string> Get(string id);

    Task<string> List();

    Task<string> Delete(string id);
}

public class ItemClient : IItemClient
{
    public HttpClient Client { get; }

    public ItemClient(HttpClient client, IConfigService configService)
    {
        var cOnfig= configService.Get();
        if (cOnfig== null)
        {
            return;
        }

        client.BaseAddress = new Uri(config.Endpoint);

        Client = client;
    }

    public async Task<string> Create(ItemForm form)
    {
        var cOntent= new StringContent(JsonConvert.SerializeObject(form), Encoding.UTF8, "application/json");
        var result = await Client.PostAsync("/api/items", content);

        if (result.IsSuccessStatusCode)
        {
            var stream = await result.Content.ReadAsStreamAsync();
            var item = Deserialize(stream);
            return $"Item created, info:{item}";
        }

        return "Error occur, please again later.";
    }

    public async Task<string> Get(string id)
    {
        var result = await Client.GetAsync($"/api/items/{id}");

        if (result.IsSuccessStatusCode)
        {
            var stream = await result.Content.ReadAsStreamAsync();
            var item = Deserialize(stream);

            var respOnse= new StringBuilder();
            response.AppendLine($"{"Id".PadRight(40, ' ')}{"Name".PadRight(20, ' ')}Age");
            response.AppendLine($"{item.Id.PadRight(40, ' ')}{item.Name.PadRight(20, ' ')}{item.Age}");
            return response.ToString();
        }

        return "Error occur, please again later.";
    }

    public async Task<string> List()
    {
        var result = await Client.GetAsync($"/api/items");

        if (result.IsSuccessStatusCode)
        {
            var stream = await result.Content.ReadAsStreamAsync();
            var items = Deserialize>(stream);

            var respOnse= new StringBuilder();
            response.AppendLine($"{"Id".PadRight(40, ' ')}{"Name".PadRight(20, ' ')}Age");

            if (items != null && items.Count > 0)
            {
                foreach (var item in items)
                {
                    response.AppendLine($"{item.Id.PadRight(40, ' ')}{item.Name.PadRight(20, ' ')}{item.Age}");
                }
            }
            
            return response.ToString();
        }

        return "Error occur, please again later.";
    }

    public async Task<string> Delete(string id)
    {
        var result = await Client.DeleteAsync($"/api/items/{id}");

        if (result.IsSuccessStatusCode)
        {
            return $"Item {id} deleted.";
        }

        if (result.StatusCode == HttpStatusCode.NotFound)
        {
            return $"Item {id} not found.";
        }

        return "Error occur, please again later.";
    }

    private static T Deserialize(Stream stream)
    {
        using var reader = new JsonTextReader(new StreamReader(stream));
        var serializer = new JsonSerializer();
        return (T)serializer.Deserialize(reader, typeof(T));
    }
}

 

如何发布

  在项目文件中设置发布程序的名称(AssemblyName):

  
    Exe
    netcoreapp3.1
    api-cli
  

 

  进入控制台程序目录:

  cd src/NetCoreCLI

 

  发布Linux使用版本:

  dotnet publish -c Release -r linux-x64 /p:PublishSingleFile=true

 

  发布Windows使用版本:

  dotnet publish -c Release -r win-x64 /p:PublishSingleFile=true

 

  发布MAC使用版本:

    dotnet publish -c Release -r osx-x64 /p:PublishSingleFile=true

 

使用示例

  这里使用Linux作为示例环境。

  1. 以docker的方式启动web api

  

 

  2. 虚拟机上没有安装.net core的环境

  

 

  3. 把编译好的CLI工具拷贝到虚拟机上,授权并移动到PATH中(如果不移动,可以通过./api-cli的方式调用)

    sudo chmod +x api-cli #授权
    sudo mv ./api-cli /usr/local/bin/api-cli #移动到PATH

 

  4. 设置配置文件:api-cli config set

  

 

  5. 查看配置文件:api-cli config get

  

 

  6. 创建条目:api-cli item create 

  

 

  7. 条目列表:api-cli item list

  

 

  8. 获取条目:api-cli item get

  

 

  9. 删除条目:api-cli item delete

  

 

  10. 指令帮助:api-cli -h, api-cli config -h, api-cli item -h

  

  

  

 

   11. 错误指令:api-cli xxx

  

 

源码地址

  https://github.com/ErikXu/NetCoreCLI

 

参考资料

  https://docs.microsoft.com/en-us/dotnet/core/rid-catalog#using-rids](https://docs.microsoft.com/en-us/dotnet/core/rid-catalog#using-rids

  https://medium.com/swlh/build-a-command-line-interface-cli-program-with-net-core-428c4c85221


推荐阅读
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • 本文介绍了一个在线急等问题解决方法,即如何统计数据库中某个字段下的所有数据,并将结果显示在文本框里。作者提到了自己是一个菜鸟,希望能够得到帮助。作者使用的是ACCESS数据库,并且给出了一个例子,希望得到的结果是560。作者还提到自己已经尝试了使用"select sum(字段2) from 表名"的语句,得到的结果是650,但不知道如何得到560。希望能够得到解决方案。 ... [详细]
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • http:my.oschina.netleejun2005blog136820刚看到群里又有同学在说HTTP协议下的Get请求参数长度是有大小限制的,最大不能超过XX ... [详细]
  • Webmin远程命令执行漏洞复现及防护方法
    本文介绍了Webmin远程命令执行漏洞CVE-2019-15107的漏洞详情和复现方法,同时提供了防护方法。漏洞存在于Webmin的找回密码页面中,攻击者无需权限即可注入命令并执行任意系统命令。文章还提供了相关参考链接和搭建靶场的步骤。此外,还指出了参考链接中的数据包不准确的问题,并解释了漏洞触发的条件。最后,给出了防护方法以避免受到该漏洞的攻击。 ... [详细]
  • springmvc学习笔记(十):控制器业务方法中通过注解实现封装Javabean接收表单提交的数据
    本文介绍了在springmvc学习笔记系列的第十篇中,控制器的业务方法中如何通过注解实现封装Javabean来接收表单提交的数据。同时还讨论了当有多个注册表单且字段完全相同时,如何将其交给同一个控制器处理。 ... [详细]
  • 利用Visual Basic开发SAP接口程序初探的方法与原理
    本文介绍了利用Visual Basic开发SAP接口程序的方法与原理,以及SAP R/3系统的特点和二次开发平台ABAP的使用。通过程序接口自动读取SAP R/3的数据表或视图,在外部进行处理和利用水晶报表等工具生成符合中国人习惯的报表样式。具体介绍了RFC调用的原理和模型,并强调本文主要不讨论SAP R/3函数的开发,而是针对使用SAP的公司的非ABAP开发人员提供了初步的接口程序开发指导。 ... [详细]
author-avatar
手机用户2502907707
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有