使用.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的方式調用)

  1.  
    sudo chmod + x api-cli #授權
  2.  
    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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM