Komut satırı uygulama modeli(console/terminal) her yazılımcının oldukça aşina olduğu bir yazılım modelidir sanırım. Yazılım ile uğraşan herkesin bir şekilde belki de başlangıç noktası… Basit bir “Hello World” çıktısını konsola yazdırmak çeşitli yazılım dillerini öğrenmek için olmazsa olmaz 🙂

node –help

Son kullanıcı ihtiyaçları açısından ve kullanıcı etkileşimi açısından artık çok tercih edilmese de, basit ve hızlı bazı görevlerin gerçekleştirilmesinde tercih ediyoruz hala. Geliştiricilerin, belli “proof of concept” çalışmaları, denemeleri ve geliştirme araçlarının çeşitli özellikleri açısından daha aşina olduğu bir model. Konsol ya da terminallere yazdığımız komutlarla bir çok uygulama ile bir çok temel işlemi yapabiliyoruz. Eminim bir çoğumuzun oldukça aşina olduğu bazı araçların, konsollarda çalıştırdığımız komut satırı fonksiyonları gündelik hayatımızın bir parçasıdır.

dotnet build –help

Bu basit girişten sonra çok da uzatmadan gelelim konumuza…

Bir çok geliştirme araçının komut satırı komutlarının çıktılarının benzerliği ya da bazı komutların standart olması dikkatinizi çektmiştir sanırım. Bir çok uygulamanın komut satırı arayüzlerinde(command-line interface, CLI) –help, –info komutlarının standart olması gibi.

Command Line API

.NET Core ile birlikte bir çok API ya da kütüphane de ortaya çıkıyor, ve bu kütüphaneler bir çok temel işlemi kolayca gerçekleştirmemizi sağlıyor. Bunlardan biri de Command Line API ve System.CommandLine… Bu API ile yukarıda bahsetmiş olduğum konsol uygulamalarının ya da komut satırı uygulamalarının temel işlemleri çok daha kolay bir şekilde yapabiliyoruz.

Şimdi küçük bir uygulama ile Command Line API’ı biraz daha yakından tanıyalım. Bu API’ın temel sınıfı CommandLineBuilder, bütün olay bu sınıf çevresinde dönüyor. Adından da anlaşılacağı gibi komut satırı akışımızı bu sınıf ile hallediyoruz. Basit bir şekilde aşağıdaki gibi bir Main() metodumuz olsun.

    class Program
    {
        private static Task<int> Main(string[] args)
        {
            ReportCommand rcmd = new ReportCommand("report");
            PrintCommand pcmd = new PrintCommand("print");

            Parser parser = new CommandLineBuilder(new Command("konsol"))
                .AddCommand(rcmd)
                .AddCommand(pcmd)
                .UseHelp()
                .UseDefaults()
                .Build();

            return parser.InvokeAsync(args);
        }
    }

Bu senaryo için ReportCommand ve PrintCommand diye, report ve print adıyla iki örnek komut oluşturdum(birazdan ayrıntılarına gireceğim) ve bu komutları CommandLineBuilder()’a konsol adı altında çalıştırılacak komutlar olarak ekliyoruz.

Kütüphanenin sağladığı bazı kolaylıkları da CommandLineBuilder() ile birleştirip uygulamamızı hazır hale getiriyoruz.

dotnet run -- --help 
dotnet run -- --version

yazarak projemizi çalıştırdığımız zaman aşağıdaki gibi bir çıktı karşımıza çıkacak. Dikkat ederseniz ki bu çıktıyı oluşturmak için herhangi bir Console.WriteLine() şeklinde kod yazmadık.

Bu help çıktısı, bize 2 tane komutun(report,print) olduğunu, bu ikisinin de bir tane argüman aldığını söylüyor.Şimdi de report komutu için olan help kısmına bakalım.

dotnet run — report –help

Bu çıktıya göre de report komutunun argüman olarak alabileceği seçenekleri ve option olarak -pid ya da –product-id adıyla bir değer alabileceğini görüyoruz.

Biraz daha netleşmesi için yukarda birazdan ayrıntılarına gireceğim dediğim komutlardan ReportCommand sınıfına bakalım. Bu System.CommandLine kütüphanesini kullanarak bizim yazdığımız özel bir sınıf. System.CommandLine API amacı daha güvenilir ve gerçekten amaça yönelik fonksiyonlara odaklanmak olduğu için, gerçekten ne yapmak istiyorsak onu yazıp, bu kütüphanenin özellikleri ile de çıktıları üretebiliyoruz. Neyse biz ReportCommand’a dönelim.


    public class ReportCommand : Command
    {
        public ReportCommand(string name, string description = null) : base(name, description)
        {
            this.AddOption(new Option(new[] { "-pid", "--product-id" })
            {
                Description = "Get id of the product",
                Argument = new Argument<int>("pid"),
            });

            this.AddArgument(new Argument<List<string>>
            {
                Name = "columns",
                Description = $"Add column names for output report.Columns: Name - Origin - Code - Price - StockDate - Count",
                Arity = ArgumentArity.ZeroOrMore
            });

            this.Handler = CommandHandler.Create<CancellationToken, List<string>, IConsole, int>(new Report().Generate);
        }

        class Report
        {
            public async Task<int> Generate(CancellationToken ct, List<string> columns, IConsole console, int productId)
            {
                try
                {
                    if (productId <= 0)
                    {
                        Console.WriteLine("Invalid product");
                        return 1;
                    }

                    Console.WriteLine("Press q to quit.");
                    return await FetchReport(productId, columns);
                }
                catch (Exception)
                {
                    return 1;
                }
            }

            private async Task<int> FetchReport(int productId, List<string> columns = null)
            {
                var reset = new ManualResetEvent(false);
                var isFinished = false;

                Task reportTask = new Task(() =>
                {
                    try
                    {
                        //Some simple loading proccess demo
                        Console.WriteLine($"Report is for: {productId.ToString() }");
                        if (columns != null &amp;&amp; columns.Any())
                        {
                            Console.WriteLine($"Columns: { string.Join(";", columns.ToArray())}");
                        }
                        Console.Write("Loading...");
                        for (int i = 0; i < 25; i++)
                        {
                            Thread.Sleep(2000);
                            Console.Write(".");
                            Console.SetCursorPosition(Console.CursorLeft, Console.CursorTop);

                        }
                        Console.WriteLine("Completed.");
                        isFinished = true;
                        //Process end

                    }
                    catch (Exception)
                    {
                        Console.WriteLine("Some error is occured.");
                    }
                    finally
                    {
                        reset.Set();
                    }
                });

                reportTask.Start();


                while (!isFinished)
                {
                    if (reset.WaitOne(250))
                    {
                        return 0;
                    }
                    if (Console.KeyAvailable)
                    {
                        break;
                    }
                    ConsoleKey cmd = Console.ReadKey(true).Key;
                    if (cmd == ConsoleKey.Q)
                    {
                        break;
                    }
                }
                return await Task.FromResult(0);
            }
        }
    }

CommandLineBuilder(), Command tipinde komutları alıyor. Dolayısıyla yaratacağımız bu komutun da çeşitli özellikleri ve argümanları oluyor. .AddOption(), .AddArgument() ile bunları oluşturup, bunları komutumuza ekliyoruz.

En önemli kısım Command’ların Handler özelliği. Bu özellik ile komutuz çağrıldığı zaman ne olacağını belirtiyoruz. ICommandHandler arayüzünden kendimiz de yaratabiliriz ya da CommandHandler.Create<>(action) ile API sayesinde yaratabiliriz ki bu daha kolay.

Burada dikkat edilmesi gereken olay, .Create<>(action) metodundaki generic parametre sırası ile action parametresindeki metot parametre sırasının aynı olması lazım. Report sınıfındaki Generate() metodu ile beraber bakarsanız sanırım biraz daha net anlaşılacaktır.

Çok daha fazla uzatmadan sizi GitHub projesi ile baş başa bırakmak isterim. Projeyi kurcalayarak burada bahsetmediğim başka özellikleri de merak edip, deneyebilirsiniz.

Siz de projenizde bu tarz yapıları kullanmak isterseniz, System.CommandLine API’ı nuget’den çekmeniz gerekiyor. .NET Core’un direk içinde olan bir API değil.

dotnet add package System.CommandLine.Experimental –version 0.3.0-alpha.19317.1

Nuget paketinin adı Experimental olması ve versiyonun *.alpha olması biraz akıllarda soru işarete yaratabilir. Ama bir çok dotnet aracı içinde hali hazırda bu API kullanılmakta.

Ektsra

Bu tarz uygulamalar için daha basit, tek komutlu yapılar için biraz daha basit bir API daha var. System.CommandLine.DragonFruit

static void Main(string[] args)
{
    Console.WriteLine("Hello World!");
}

Standart bir konsol uygulamasında Main() metodu içinde argüman olarak ifadeleri verebiliriz. Bir string[]’i ile -argüman olarak verdiğimiz değerleri konsol uygulamamıza dahil edebiliriz. Peki bu argümanları tanımlı birer metot parametresi olarak alabilsek ve daha kolay işleyebilsek ve ek olarak bunların nasıl kullanıldığını da kolayca konsolda gösterebilsek güzel olmaz mıydı?

Bence çok güzel olurdu. Bunun için System.CommandLine.DragonFruit’u sadece projemize eklememiz ile; aşağıdaki gibi bir konsol uygulamamızın çıktısı yine aşağıdaki gibi olacaktır.

using System;

namespace Konsol
{
    class Program
    {
        /// <summary>
        /// Do you remember mIRC?
        /// </summary>
        /// <param name="age">Age of the person</param>
        /// <param name="sex">Gender of the person</param>
        /// <param name="location">Location of the person</param>
        static void Main(int age = 18, string sex = "male", string location = "ankara")
        {
            Console.WriteLine($"Age: {age}");
            Console.WriteLine($"Sex {sex}");
            Console.WriteLine($"Location: {location}");
        }
    }
}

mIRC hatırlayanlar var mı? 😀

Yine projenize nuget’den çekmeniz gerekiyor. Projenin referanslarında olması yeterli. Kod tarafında farklı bir şey yapmaya da gerek yok.

dotnet add package System.CommandLine.DragonFruit –version 0.3.0-alpha.19317.1

.NET’in bu Command Line API’ı diyebileceğimiz bu özellikleri de tabi ki açık kaynak olarak geliştirilmekte. GitHub‘dan katkıda bulunabilirsiniz.

Geliştirdiğiniz uygulamaların bazı özelliklerini CLI üzerinden sunup, kolay kullanımını bu şekilde sağlayabilirsiniz. Kendi kendini anlatan bu tarz uygulamalar ile ciddi anlamda bazı işleri çok kolay çözümlemek ciddi anlamda kazançlar getiriyor.

Bir sonraki yazıya kadar, keyifli kodlamalar. 🙂