使用.Net Core编写命令行工具(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<List<Item>>(_key);
    return Ok(items);
  }

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

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

    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<List<Item>>(_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<IConfigService, ConfigService>();
    serviceCollection.AddHttpClient<IItemClient, ItemClient>();

    var services = serviceCollection.BuildServiceProvider();

    var app = new CommandLineApplication<Program>();
    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<Config>(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<Item>(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<Item>(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<List<Item>>(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<T>(Stream stream)
  {
    using var reader = new JsonTextReader(new StreamReader(stream));
    var serializer = new JsonSerializer();
    return (T)serializer.Deserialize(reader, typeof(T));
  }
}

如何发布

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

 <PropertyGroup>
   <OutputType>Exe</OutputType>
   <TargetFramework>netcoreapp3.1</TargetFramework>
   <AssemblyName>api-cli</AssemblyName>
  </PropertyGroup>

  进入控制台程序目录:

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

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

到此这篇关于使用.Net Core编写命令行工具(CLI)的方法的文章就介绍到这了,更多相关.Net Core 命令行工具内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C#/.NET使用git命令行来操作git仓库的方法示例

    我们可以在命令行中操作 git,但是作为一名程序员,如果在大量重复的时候还手动敲命令行,那就太笨了. 本文介绍使用 C# 编写一个 .NET 程序来自动化地使用 git 命令行来操作 git 仓库. 这是一篇很基础的入门文章. 最简单的运行 git 命令的代码 在 .NET 中,运行一个命令只需要使用 Process.Start 开启一个子进程就好了.于是要运行一个 git 命令,我们其实只需要这句足以: Process.Start("git", "status")

  • .NET命令行解析器示例程序(命令行选项功能)

    示例需求 拷贝文件,如:CopyFiles -s "E:\Framework\Tenoner - 副本 (2)" -p "*.csproj" -t "E:\Framework\Tenoner - 副本 (2)\Bak",可以支持:深度拷贝.拷贝符合指定模式的文件.是否覆盖等选项. 使用 CommandLineParserCommandLineParser 是一个轻量级的工具,使用非常简答,官方也有教程. 选项类 复制代码 代码如下: using

  • VS.net VSS时,编译报错:未能向文件“.csproj.FileListAbsolute.txt”写入命令行 对路径 的访问被拒绝。

    前段时间由于VSS上的一个项目要给2个公司开发使用,而2个公司的需求不同 就把该项目复制到VSS上的另外一个目录,结果在别人的电脑取出并打开这个项目编译时,报错: 未能向文件".csproj.FileListAbsolute.txt"写入命令行 对路径 的访问被拒绝... 经查,发现错误所说的这个文件:.csproj.FileListAbsolute.txt,是只读属性 再对比VSS上的这2个项目,发现后面的项目在VSS上多了2个目录: Bin和Obj 明白原因了,在复制时,把这2个生

  • 使用.NET命令行编译器编译项目(如ASP.NET、C#等)

    源程序最好有.csproj或.vbproj文件,没有的话,要花些时间调试 下面我以VB.NET做示例讲解一下: 从proj我们可以获取以下有用信息 Settings小节中有很多配置选项,对应一些编译器选项 <References>小节中是项目的引用,第3方类库最好用绝对路径 <Imports>小节中是要导入的一些命名空间 <Files>小节中有项目的所有文件,选取 BuildAction = "Compile"的文件 用vbc测试了一下,很容易,注意

  • 使用.Net Core编写命令行工具(CLI)的方法

    命令行工具(CLI) 命令行工具(CLI)是在图形用户界面得到普及之前使用最为广泛的用户界面,它通常不支持鼠标,用户通过键盘输入指令,计算机接收到指令后,予以执行. 通常认为,命令行工具(CLI)没有图形用户界面(GUI)那么方便用户操作.因为,命令行工具的软件通常需要用户记忆操作的命令,但是,由于其本身的特点,命令行工具要较图形用户界面节约计算机系统的资源.在熟记命令的前提下,使用命令行工具往往要较使用图形用户界面的操作速度要快.所以,图形用户界面的操作系统中,都保留着可选的命令行工具. 另外

  • 一文秒懂Go 编写命令行工具的代码

    前言 最近因为项目需要写了一段时间的 Go ,相对于 Java 来说语法简单同时又有着一些 Python 之类的语法糖,让人大呼"真香". 但现阶段相对来说还是 Python 写的多一些,偶尔还得回炉写点 Java :自然对 Go 也谈不上多熟悉. 于是便利用周末时间自己做个小项目来加深一些使用经验.于是我便想到了之前利用 Java 写的一个博客小工具. 那段时间正值微博图床大量图片禁止外链,导致许多个人博客中的图片都不能查看.这个工具可以将文章中的图片备份到本地,还能将图片直接替换到

  • 浅谈node.js 命令行工具(cli)

    一. 先了解一下package.json 每个项目的根目录都有一个 package.json 文件,定义了这个项目所需要的各种模块,以及项目的配置信息,下面是一个比较完整的package.json文件 { "name": "vue-cli", "version": "2.9.3", "description": "A simple CLI for scaffolding Vue.js projec

  • 使用Python编写类UNIX系统的命令行工具的教程

    引言 您是否能编写命令行工具?也许您可以,但您能编写出真正好用的命令行工具吗?本文讨论使用 Python 来创建一个强健的命令行工具,并带有内置的帮助菜单.错误处理和选项处理.由于一些奇怪的原因,很多人并不了解 Python? 的标准库具有制作功能极其强大的 *NIX 命令行工具所需的全部工具. 可以这样说,Python 是制作 *NIX 命令行工具的最佳语言,因为它依照"batteries-included"的哲学方式工作,并且强调提供可读性高的代码.但仅作为提醒,当您发现使用 Py

  • node命令行工具之实现项目工程自动初始化的标准流程

    一.目的 传统的前端项目初始流程一般是这样: 可以看出,传统的初始化步骤,花费的时间并不少.而且,人工操作的情况下,总有改漏的情况出现.这个缺点有时很致命. 甚至有马大哈,没有更新项目仓库地址,导致提交代码到旧仓库,这就很尴尬了... 基于这些情况,编写命令行工具(CLI)的目的就很明确: 用于新项目工程的初始化利用工具进行初始化,可以节省项目初期的准备时间避免出现改漏的情况杜绝未更新项目版本仓库地址的问题 以下是新的流程示意图: 二.自动化流程分析 以下是自动化流程图: 从流程图可以得出两个重

  • 详解Node.js如何开发命令行工具

    前言 Node 给前端开发带来了很大的改变,促进了前端开发的自动化,我们可以简化开发工作,然后利用各种工具包生成生产环境.如运行sass src/sass/main.scss dist/css/main.css即可编译 Sass 文件. 在实际的开发过程中,我们可能会有自己的特定需求, 那么我们得学会如何创建一个Node命令行工具. hello world 老规矩第一个程序为hello world.在工程中新建bin目录,在该目录下创建名为helper的文件,具体内容如下: #!/usr/bin

  • Python编程编写完善的命令行工具

    目录 1. python-fire 2. mando 最后的话 1. python-fire python-fire 是一个三方库,可以将任何 Python 对象变成一个命令行接口. 使用前先 pip install fire 下. 可以把你的函数直接变成命令行接口: import fire def hello(name="World"): return "Hello %s!" % name if __name__ == '__main__': fire.Fire(

  • Nodejs 发布自己的npm包并制作成命令行工具的实例讲解

    <span style="font-family:Arial, Helvetica, sans-serif;background-color:rgb(255,255,255);">近日当我在使用npm上已经存在的一个包时,发现它有bug:于是决定自己实现这个功能,自己写一个npm包.</span> 下面我记录一下自己的实现过程. 1. npm init 选择一个文件夹,然后用命令行cd进去,然后执行npm init,这时会生成一长串表单,根据自己的实际情况填写内

  • Linux 命令行工具解析和格式化输出 JSON的方法

    JSON 是一种轻量级且与语言无关的数据存储格式,易于与大多数编程语言集成,也易于人类理解 -- 当然,如果格式正确的话.JSON 这个词代表 J ava S cript O bject N otation,虽然它以 JavaScript 开头,而且主要用于在服务器和浏览器之间交换数据,但现在正在用于许多领域,包括嵌入式系统.在这里,我们将使用 Linux 上的命令行工具解析并格式化打印 JSON.它对于在 shell 脚本中处理大型 JSON 数据或在 shell 脚本中处理 JSON 数据非

  • 详解Go 创建命令行工具的方法

    前言 最近因为项目需要写了一段时间的 Go ,相对于 Java 来说语法简单同时又有着一些 Python 之类的语法糖,让人大呼"真香". 但现阶段相对来说还是 Python 写的多一些,偶尔还得回炉写点 Java :自然对 Go 也谈不上多熟悉. 于是便利用周末时间自己做个小项目来加深一些使用经验.于是我便想到了之前利用 Java 写的一个博客小工具. 那段时间正值微博图床大量图片禁止外链,导致许多个人博客中的图片都不能查看.这个工具可以将文章中的图片备份到本地,还能将图片直接替换到

随机推荐