Створюємо Command-Line Interface додаток за допомогою .NET Core і розповсюджуємо як .NET tools
Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.
Привіт, мене звати Антон. Я працюю .NET-розробником у компанії Sitecore Ukraine у команді Developer Experience, яка створює додатки як для сторонніх Sitecore-розробників, так і для власних потреб компанії, що допомагають полегшити розробку та DevOps-процеси.
Більшість із нас використовують Command-line interface (CLI-) додатки щодня, але мало хто замислювався над створенням власних, окрім примітивних програм на перших курсах університету.
У цій статті я розповім про .NET Core Global Tools і наведу кілька прикладів імплементації простої програми.
Навіщо мені створювати власний CLI-додаток
Однією з головних переваг консольного додатку є те, що він може приймати всі вхідні дані як аргументи, а отже не потребує взаємодії з користувачем. Як результат, це дозволяє використовувати
Переваги розповсюдження у форматі .NET tool
З релізом .NET Core 2.1 SDK ми отримали нову фічу під назвою .NET Core Global Tools, яка дає нам змогу створювати кросплатформені
.NET tool — це спеціальний nuget package, що містить консольний додаток, який можна встановити як global tool чи local tool. Завдяки такому формату пакування ви отримуєте всі переваги одного з найпоширеніших менеджерів пакетів. Серед них:
- Повна підтримка роботи з версіями:
- Зберігання необмеженої кількості версій вашого додатку.
- Використання різних версій додатку для різних продуктів.
- «Замороження» використання певної версії додатку, яка сумісна з певною версією вашого продукту. При цьому можна продовжувати розробку додатку для наступних версій продукту і вносити зміни, які могли б зламати попередні версії продукту.
- Використання Pre-release версій, щоб показати, що ця версія додатку ще в роботі.
- Використання nuget.org як способу розповсюдження вашого додатку:
- При застосуванні nuget.org ви можете використовувати ваш додаток у будь-якому процесі (на локальному комп’ютері, на сервері в локальній мережі або ж навіть при хмарних обчисленнях). Головне, щоб процес мав доступ до інтернету.
- Не потрібно знаходити місце (локальна папка, папка зі спільним доступом на сервері...) для зберігання додатку.
- Якщо ж ви не хочете робити ваш додаток публічним, у вас є опції розміщення його на локальному nuget сервері, або ж (у найгіршому разі) в папці на диску вашого комп’ютера, або сервера в мережі.
- Набір інструментів від Microsoft сам визначить, які
dll-файли (окрім вашого) потрібно встановити, щоб додаток працював на якомусь конкретному комп’ютері. Отже, пакування та інсталяція додатку стають набагато простішими.
Приклад встановлення як Global Tool
dotnet tool install -g myfirstcli
Результат:
You can invoke the tool using the following command: myfirstcli Tool 'myfirstcli' (version '1.0.0') was successfully installed.
Приклад встановлення як Local Tool
Щоб встановити засіб лише для локального доступу (для поточного каталогу та підкаталогів), його необхідно додати до файлу маніфесту засобу. Щоб створити файл маніфесту засобу, виконайте наступну команду:
dotnet new tool-manifest
Результат:
The template "Dotnet local tool manifest file" was created successfully.
Локально у директорії .config ви знайдете dotnet-tools.json
файл з наступним :
{ "version": 1, "isRoot": true, "tools": { } }
dotnet tool install myfirstcli:
Результат:
You can invoke the tool from this directory using the following commands: 'dotnet tool run myfirstcli' or 'dotnet myfirstcli'. Tool 'myfirstcli' (version '1.0.0') was successfully installed. Entry is added to the manifest file C:\examples\Test\.config\dotnet-tools.json. dotnet-tools.json: { "version": 1, "isRoot": true, "tools": { "myfirstcli": { "version": "1.0.0", "commands": [ "myfirstcommand" ] } } }
Створюємо власний .NET tool application
Далі я наведу приклад створення звичайного консольного додатку CopyrightsCheck.Cli, який можна використовувати у CI/CD процесах. Головним функціоналом програми буде перевірка копірайт-хедерів у заданих файлах і створення файлу з репортом.
Для створення .NET tool application треба використовувати шаблон консольного додатку. Простіше за все створити його з терміналу за допомогою наступної команди:
dotnet new console -n myfirstcli -f net6.0
Ця команда створює консольний проєкт з потрібною назвою і версією .NET. Наступним кроком буде конфігурування проєкту як .NET tool. Ключовим налаштуванням тут буде true, яке допоможе пакувати додаток як .NET tool, що дасть вам можливість надалі легко встановити наш додаток за допомогою команди dotnet tool install :
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net6.0</TargetFramework> <PackAsTool>true</PackAsTool> <ToolCommandName>copyrightsManager</ToolCommandName> <ItemGroup> <PackageReference Include="System.CommandLine.Hosting" Version="0.3.0-alpha.21216.1" /> <PackageReference Include="Spectre.Console" Version="0.43.1-preview.0.2" /> </ItemGroup> </PropertyGroup> </Project>
У нашому тестовому проєкті ми будемо використовувати System.CommandLine бібліотеку. Вона є дуже потужним інструментом, що допомагає створювати та інтерпретувати команди нашого консольного додатку. System.CommandLine.Hosting допомагає налаштувати наш додаток та Spectre.Console — вона знадобиться, щоб зробити вашу консоль більш виразною.
Program.cs :
var parser = BuildCommandLine() .UseHost(_ => Host.CreateDefaultBuilder(args), (builder) => { builder .ConfigureServices(services => { services.TryAddSingleton<ICopyrightsService, CopyrightsService>(); }) .ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()) .UseCommandHandler<CopyrightsCheckCommand, CopyrightsCheckCommand.Handler>(); }).UseDefaults().Build(); return await parser.InvokeAsync(args); static CommandLineBuilder BuildCommandLine() { var root = new RootCommand("Use this tool to work with copyrights."); var copyrightsCheckCommand = new CopyrightsCheckCommand(); root.AddCommand(copyrightsCheckCommand); return new CommandLineBuilder(root); }
Як видно з коду program.cs файла (так, це новий спрощений синтаксис .net 6.0, не дивуйтесь), конфігурація відбувається у розширенні UseHost для CommandLineBuilder, де ми можемо налаштувати сервіси, роботу з конфігураційними файлами тощо. У статичному методі BuildCommandLine налаштовується головна команда.
Далі розглянемо реалізацію команди CopyrightsCheckCommand, яку ми додали в головному файлі:
CopyrightsCheckCommand:
class CopyrightsCheckCommand : Command { public CopyrightsCheckCommand() : base("check", "Checks copyrights header in selected files.") { AddOption(new Option<string>(new[] { "--copyright", "-c" }, "Copyright text.") { IsRequired = true }); AddOption(new Option<string[]>(new[] { "--includePaths", "-ip" }, "Included paths.") { IsRequired = true }); AddOption(new Option<string[]>(new[] { "--excludePaths", "-ep" }, "Excluded paths.")); AddOption(new Option<string>(new[] { "--extensions", "-e" }, "File extensions.") { IsRequired = true }); AddOption(new Option<string>(new[] { "--result", "-r" }, "Copyrights check results directory.")); } public new class Handler : ICommandHandler { public string Copyright { get; set; } public string[] IncludePaths { get; set; } public string[] ExcludePaths { get; set; } public string[] Extensions { get; set; } public string Result { get; set; } private readonly ICopyrightsService _copyrightsService; public Handler(ICopyrightsService copyrightsService) { _copyrightsService = copyrightsService; } public async Task<int> InvokeAsync(InvocationContext context) { var copyrightsCheckResults = await _copyrightsService.CheckCopyrights(IncludePaths, ExcludePaths, Extensions, Copyright); AnsiConsole.Write( new FigletText($"Copyrights Check Result:") .Centered() .Color(Color.Aqua)); var copyrightsCheckTable = new Table() .Border(TableBorder.HeavyEdge) .BorderColor(Color.Green) .AddColumn(new TableColumn("File Name")) .AddColumn(new TableColumn("File Directory")) .AddColumn(new TableColumn("Copyrights Check Result").Centered()); foreach (var copyrightsCheckResult in copyrightsCheckResults) { copyrightsCheckTable.AddRow( copyrightsCheckResult.FileName, copyrightsCheckResult.FileDirectory, copyrightsCheckResult.CheckResult ? $"[blue]{copyrightsCheckResult.CheckResult}[/]" : $"[red]{copyrightsCheckResult.CheckResult}[/]"); } AnsiConsole.Write(copyrightsCheckTable); if (!string.IsNullOrWhiteSpace(Result)) { var resultFileName = $"{Result}{Path.DirectorySeparatorChar}copyrightsCheckResult.json"; await using FileStream createStream = File.Create(resultFileName); await JsonSerializer.SerializeAsync(createStream, copyrightsCheckResults); AnsiConsole.Write(new Text($"Copyrights check result saved to file: {resultFileName}", new Style(Color.Aqua))); } return copyrightsCheckResults.All(res => res.CheckResult) ? 0 : 1; } } }
Як бачите, всі налаштування містяться всередині команди, чи це назва та опис команди, чи конфігурування додаткових опцій, весь цей функціонал можливий за допомоги System.CommandLine бібліотеки. Це дає справді чистий вигляд консольних команд, які легко розширювати і тестувати, позбавляє потрібності «парсити» усе за допомогою кастомного коду. Також зверніть увагу, як легко вивести дані у табличному вигляді за допомогою Spectre.Console.
Реалізація ICopyrightsService :
class CopyrightsService : ICopyrightsService { private readonly IFileManager _fileManager; public CopyrightsService(IFileManager fileManager) { _fileManager = fileManager; } public async Task<List<CopyrightsCheckResult>> CheckCopyrights(string[] includePaths, string[] excludePaths, string[] extensions, string copyrightsText) { var result = new List<CopyrightsCheckResult>(); var extensionsPattern = string.Join('|', extensions); foreach (var includePath in includePaths) { if (_fileManager.DirectoryExist(includePath)) { var files = _fileManager.GetFiles(includePath, extensionsPattern); foreach (var file in files) { if (excludePaths!=null && excludePaths.Any(file.Contains)) { continue; } var fileInfo = new FileInfo(file); using StreamReader reader = _fileManager.GetReader(file); var line = await reader.ReadLineAsync(); result.Add(new CopyrightsCheckResult { CheckResult = line == copyrightsText, FileName = fileInfo.Name, FileDirectory = fileInfo.DirectoryName }); } } } return result; } }
Результат виконання команди dotnet copyrightsManager -h:
Ми бачимо, що команда з’явились у нашому додатку.
Результат виконання команди dotnet copyrightsManager check -h:
Результат виконання команди:
dotnet copyrightsManager check -ip C:\examples\CopyrightsCheck\src -ep C:\examples\CopyrightsCheck\src\obj C:\examples\CopyrightsCheck\src\bin -e *.cs -c "// Copyright (C) 2022 - All Rights Reserved" -r C:\examples:
Також ми отримали файл copyrightsCheckResult.json з результатами перевірки, який можна додати до білд артефактів:
[ { "FileName": "Program.cs", "FileDirectory": "C:\\examples\\CopyrightsCheck\\src", "CheckResult": false }, { "FileName": "CopyrightsCheckCommand.cs", "FileDirectory": "C:\\examples\\CopyrightsCheck\\src\\Commands", "CheckResult": true }, { "FileName": "CopyrightsCheckResult.cs", "FileDirectory": "C:\\examples\\CopyrightsCheck\\src\\Models", "CheckResult": false }, { "FileName": "CopyrightsService.cs", "FileDirectory": "C:\\examples\\CopyrightsCheck\\src\\Services", "CheckResult": false }, { "FileName": "ICopyrightsService.cs", "FileDirectory": "C:\\examples\\CopyrightsCheck\\src\\Services", "CheckResult": true } ]
Повний перелік команд, потрібних для використання додатку на CI:
dotnet new tool-manifest dotnet nuget add source -n MyPackageSource https://api.mynugetserver.org/v3/index.json dotnet tool install copyrightsManager.cli dotnet copyrightsManager check -ip .\src -ep .\src\obj .\src\bin -e *cs -c "// Copyright (C) 2022 - All Rights Reserved" -r .\artifacts
Ось так, з мінімальним об’ємом коду, ми створили
У наступній статті я спробую розповісти, як зробити плагін модель для
Посилання:
- Command Line Interface Guidelines
- dotnet/command-line-api (GitHub)
- spectreconsole/spectre.console (GitHub)
Сподобалась стаття? Натискай «Подобається» внизу. Це допоможе автору виграти подарунок у програмі #ПишуНаDOU
9 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів