Parsowanie parametrów w aplikacji konsolowej za pomocą CommandLineParser

Wprowadzenie

Zapewne zastanawiasz się, czy w ogóle powinieneś lub powinnaś interesować się tworzeniem aplikacji konsolowych. Czy to jeszcze ma sens. Czy ktoś jeszcze tego używa. Od jakieś czasu widzę w swoim środowisku tendencje, w której aplikacje konsolowe przeżywają swoisty renesans. Powstaje ich coraz więcej i więcej. Jednym z powodów takiego stanu rzeczy jest to, że nasze systemy stają się coraz bardziej rozbudowane (np. mamy wiele mikroserwisów) i coraz bardziej staramy się wszystko oskryptować. A w takiej sytuacji aplikacje konsolowe sprawdzają się najlepiej.

Czasami warto zainteresować się tym, w jaki sposób, możemy coś ciekawego zrobić w aplikacji konsolowej. Dlatego postanowiłem, że w dzisiejszym wpisie pokaże Ci, w jaki sposób parsować parametry przekazywane do aplikacji konsolowej z wykorzystaniem biblioteki CommandLineParser (https://github.com/commandlineparser/commandline). Ułatwia ona cały proces, a dodatkowo dodaje kilka przydatnych rzeczy do obsługi parametrów.

Parametry aplikacji konsolowej

Uruchamiając aplikację konsolową możemy przekazać do niej parametry, które będą sterowały jej działaniem. Parametry mogą być przekazane w różny sposób. Możemy na przykład przekazać parametry bezpośrednio po nazwie uruchamianej aplikacji:

Możemy również skorzystać przełączników, gdzie parametrom nadajemy nazwy:

Możemy też skorzystać z pełnych nazw parametrów:

Jak widzisz opcji jest sporo i parsując to ręcznie, możemy napisać bardzo dużo kodu, aby obsłużyć wszystkie możliwości.

Parsowanie parametrów

W aplikacji konsolowej parametry przekazane do niej są dostępne za pomocą parametru args metody Main:

Bez problemy możemy je odczytać z tablicy, ale niestety ich sparsowanie nie jest już takie proste. Jak widać na zrzucie ekranu poniżej, bardzo często niektóry parametry zależą od siebie (np. dwa pierwsze parametry) przez co logika parsowania dość mocno się rozbudowuje.

console parameters

CommandLineParser

CommandLineParser (https://github.com/commandlineparser/commandline) bardzo ułatwia parsowanie parametrów aplikacji konsolowej. Dodatkowo dodaje obsługę pomocy w naszej aplikacji, dzięki której użytkownik może dowiedzieć się jak jej użyć.

Praca z biblioteką rozpoczyna się od utworzenie klasy do której zostaną sparsowane parametry. Za pomocą atrybutów możemy określić w jaki sposób będą parsowane właściwości. Najlepiej zobaczyć to na przykładzie. Poniżej znajduje się klasa Option z dostępnymi parametrami:

Klasa Options zawiera trzy parametry, które użytkownik może przekazać do aplikacji. Atrybuty Option oraz Value decydują w jaki sposób wartości będą przekazane do aplikacji. Option wykorzystujemy, gdy chcemy przekazać wartości z wykorzystaniem przełączników.

Pierwszy parametr w atrybucie określa krótką nazwę (np. -f), drugi pełną (np. –file). Required określa, czy parametr jest wymagany, czy nie. Gdy jest, a użytkownik nie przekaże jego wartości to biblioteka wyświetli stosowny komunikat i zakończy działanie aplikacji.

Za pomocą HelpText możemy przekazać informacje o parametrze, które wyświetlą się użytkowniku w pomocy aplikacji lub w przypadku, gdy parametr jest wymagany. Natomiast Default określa domyślną wartość parametru, gdy jest on opcjonalny.

Atrybut Value służy do sparsowanie parametrów, które nie mają nazwy. Wymaga on podania indeksu, od którego będą parsowane do niego wartości. Możemy mieć kilka właściwości z tym atrybutem, wtedy indeksy będą określały jakie wartości trafią do, której właściwości. Na przykład:

Spowoduje, że pierwszy parametr trafi do właściwości IntValue. Kolejne (od 1 do 3) do właściwości StringSeq, natomiast ostatni do właściwości DoubleValue. Osobiście unikam stosowania więcej nić jednej właściwość z atrybutem Value, bo później mogą być problemy z poprawną interpretacją parametrów, w szczególności w sytuacji, gdy korzystamy z Min i Max.

Atrybut Value ma również standardowe właściwości jak HelpText, czy Required.

Darmowy kurs Visual Studio

Pracując z setkami programistów, zauważyłem, że większość osób nie pracuje efektywnie w Visual Studio. W skrajnych przypadkach korzystali z kopiowania z wykorzystaniem menu Edit. Wiem, że to dziwne, ale naprawdę niektórzy tak pracują. Dlatego postanowiłem stworzyć kurs Visual Studio – aby pomóc koleżankom i kolegom w efektywniejszej pracy.

Przygotowałem 20 lekcji e-mail, w których pokażę Ci, w jaki sposób pracować efektywniej i szybciej w Visual Studio. Poznasz dodatki, bez których nie wyobrażam sobie pracy w tym IDE.

Po więcej informacji zapraszam na dedykowaną stronę kursu: Darmowy Kurs Visual Studio.

Quiz C#

Ostatnio przygotowałem również quiz C#, w którym możesz sprawdzić swoją wiedzę. Podejmiesz wyzwanie?

Parsowanie parametrów

Mając już przygotowaną klasę dla parametrów można ją sparsować przy starcie aplikacji konsolowej. Robi się dość prosto:

W pierwszej kolejności parsujemy parametry za pomocą biblioteki. W parametrze generycznym przekazujemy typ z parametrami, do którego mają zostać sparsowane dane. Metoda zwraca wynik parsowania. Co istotne nie jest to nasz obiekt z parametrami. Sparsowany obiekt możemy otrzymać dopiero wywołując metodę WithParsed, do której przekazujemy delegat, który ma się wykonać w momencie, gdy sparsowane dane są poprawne (np. uzupełnione wszystkie pola).

Możemy również obsłużyć sytuację, gdy parametry nie sparsowały się poprawnie. Wystarczy skorzystać z metody WithNotParsed, do której biblioteka przekaże listę błędów. Sama biblioteka wyświetla również listę błędów, więc z metody WithNotParsed na ogół nie musimy korzystać.

Uruchomienie aplikacji

Aplikacje możemy uruchamiać na różne sposoby. Może być to wiersz polecać dla skompilowanej aplikacji. Również może to być Visual Studio, szczególnie, gdy zależy nam na debugowaniu aplikacji. Domyślnie Visual Studio nie przekazuje żadnych parametrów, gdy uruchamiamy aplikację w trybie debugowania. Możemy to zmienić i określić parametry, które potrzebujemy lub które wymaga aplikacja. Robi się to we właściwościach projektu:

command parameters from visual studio

W listy zakładek z lewej strony wybieramy Debug i następnie w polu Command line arguments możemy przekazać listę parametrów, które Visual Studio przekaże do aplikacji podczas uruchamiania jej z poziomu Visual Studio. Wynik działania aplikacji z tymi parametrami wygląda tak:

command parameteres result 1

W przypadku, gdy parametry przekazane do aplikacji nie zostaną poprawnie sparsowane, biblioteka domyślnie wyświetla stosowny komunikat. Poniżej wynik działania aplikacji, gdy nie przekażemy do niej żadnego parametru:

command parameteres result with errors

Aplikacja wyświetla informacje o wymaganym parametrze ‚f, file’. Dodatkowo wyświetla również informacje o innych dostępnych opcjach. CommandLineParser automatycznie dodane dwie opcje do aplikacji. „–version” wyświetla wersję aplikacji. Jest to tak naprawdę wersja assembly, która jest ustawiona w pliku AssemblyInfo.cs.

Drugą ciekawszą opcją jest parametr „–help”. Poniżej jest wynik działania tego parametru:

command parameteres result with help

Aplikacja wyświetla informacje o wszystkich dostępnych opcjach wraz z ich opisami.

Komendy

W bardziej rozbudowanych aplikacjach możemy chcieć dodać obsługę komend. W takiej sytuacji pierwszy parametr przekazany do aplikacji określa nazwę operacji, którą chcemy wykonać. CommandLineParser wpiera parsowanie komend, przez co ich obsługa w aplikacji jest również dość prosta.

Główna różnica w stosunku do parsowanie samych parametrów polega na tym, że dla każdej komendy tworzymy klasę parametrów, którą dekorujemy atrybutem Verb. Klasy parametrów komend mogą dziedziczyć z innych klas, dzięki czemu nie musimy w każdej z klas definiować od nowa tych samych parametrów. W przykładzie przygotowałem dwie komendy:

Jak widać nie ma tutaj nic skomplikowane. Dodajemy klasy (w przykładzie klasy dziedziczą z klasy Options, więc będą miały te same parametry) i dekorujemy jest atrybutem Verb. Pierwszy parametr określa nazwę komendy, którą będziemy musieli przekazać jako pierwszy parametr.

Parsowanie komend jest dość podobne do parsowania parametrów. Różnica polega na tym, że w parsowaniu przekazujemy w formie parametrów generycznych wszystkie klasy komend. Podobnie później dla każdej klasy komendy wywołujemy metodę WithParsed:

Wynik wykonania komendy pack („pack -f result.json file1.txt file2.txt file3.txt”) wygląda tak:

command parameteres pack command

W przypadku, gdy nie przekażemy żadnej komendy do aplikacji, wtedy CommandLineParser wyświetli nam błąd oraz listę wszystkich dostępnych komend:

command parameteres result with errors verb

Następnie możemy skorzystać z komendy help, który wyświetli nam informacje o określonej komendzie (np. „help pack”):

command parameteres help pack command

Dynamiczne komendy

Taki sposób pracy z komendami z racji złamania reguły Open Closed Principle, na dłuższą metę jest dość problematyczny, gdy do aplikacji dodajemy sporo nowych komend. Na szczęście można to zorganizować trochę inaczej, aby nie trzeba było zmieniać klasy Program po dodaniu nowej komendy do aplikacji.

W tym celu do aplikacji dodaje nowy interfejs (IVerb), który służy mi do definiowania nowych komend aplikacji:

Jak widać interfejs jest bardzo prosty, zawiera tylko jedną metodę Run. Następnie do klas komend dodaje implementacje tego interfejsu:

Na końcu wystarczy zmienić trochę parsowanie parametrów w CommandLineParser:

Tym razem w pierwszej kolejności szukamy za pomocą refleksji wszystkich typów, które implementują interfejs IVerb. Następnie przekazujemy do aplikacji te typy w postaci tablicy typów.

Na końcu metodę WithParsed wywołujemy dla interfejsu, a nie konkretnej komendy, gdzie w momencie sparsowanie danych zostanie wywołana metoda Run, która wykona logikę z właściwej komendy.

Poniżej wynik wykonania komendy test („test -f result.json file1.txt file2.txt file3.txt”):

command parameteres test command

Jak widać aplikacja zachowuje się w ten sam sposób, tylko, że tym razem jest dużo przyjemniejsza w rozwoju. Dodanie nowej komendy sprowadza się to dodania nowej klasy, który implementuje interfejs IVerb oraz udekorowania ją atrybutem Verb. Idealny przykład dla reguły Open Close Principle.

Przykład

Na githubie (https://github.com/danielplawgo/CommandLineParserExample) znajduje się przykład do tego wpisu. Dla każdego z trzech przypadków przygotowałem odpowiednie metody, które należy wywoływać w metodzie Main. Przed każdą z testowych metod są podane przykłady parametrów z jakimi należy uruchomić aplikację, aby ją przetestować.

Podsumowanie

Myślę, że w najbliższym czasie coraz częściej będziemy tworzyć małe aplikacje konsolowe, które następnie będą wykonywana w ramach jakiś większych całości. Sam w ostatnio stworzyłem kilka takich drobnych aplikaci, które później zostały wykorzystane w większych skryptach, czy wywołane przez inne aplikacje. Rozmawiaj z kolegami i koleżankami, u nich też widzę powoli ten trend.

Dlatego myślę, że warto zainteresować się bibliotekami takimi jak CommandLineParser, które ułatwiają tworzenie takich aplikacji konsolowych.

1 thought on “Parsowanie parametrów w aplikacji konsolowej za pomocą CommandLineParser

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *