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 tendencję, która polega na tym, że 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 w aplikacji konsolowej zrobić. Dlatego postanowiłem, że w dzisiejszym wpisie pokażę 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:

Kolejną możliwością jest skorzystanie z przełączników, w których 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 problemu możemy je odczytać z tablicy, ale niestety ich sparsowanie nie jest już takie proste. Jak widać na poniższym zrzucie ekranu, bardzo często niektóre 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 utworzena klasy, do której zostaną sparsowane parametry. Za pomocą atrybutów możemy określić, w jaki sposób parsowane będą właściwości. Najlepiej zobaczyć to na przykładzie. Poniżej znajduje się klasa Options z dostępnymi parametrami:

Klasa Options zawiera trzy parametry, które użytkownik może przekazać do aplikacji. Atrybuty Option oraz Value decydują o tym, 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żytkownikowi w pomocy aplikacji lub wyświetlą się w przypadku, gdy parametr jest wymagany. Default określa natomiast domyślną wartość parametru, gdy jest on opcjonalny.

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

Spowoduje to, ż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ą pojawić się 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, takie 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ę to 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 przez wywołanie metody WithParsed, do której przekazujemy delegat mający się wykonać w momencie, gdy sparsowane dane są poprawne (np. uzupełnione są 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 polecenia dla skompilowanej aplikacji, może to być również 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órych potrzebujemy lub których wymaga aplikacja. Robi się to we właściwościach projektu:

command parameters from visual studio

W liście zakładek z lewej strony wybieramy Debug, a 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świetli 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 pokazuje również informacje o innych dostępnych opcjach. CommandLineParser automatycznie dodaje 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 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 również jest dość prosta.

Główna różnica w stosunku do parsowania 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 skomplikowanego. Dodajemy klasy (w przykładzie klasy dziedziczą z klasy Options, więc będą miały te same parametry) i dekorujemy je 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óra 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ć nieco inaczej, aby nie trzeba było zmieniać klasy Program po dodaniu nowej komendy do aplikacji.

W tym celu do aplikacji dodaję 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 dodaję implementacje tego interfejsu:

Na końcu wystarczy zmienić nieco 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 sparsowania 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ę do dodania nowej klasy, która implementuje interfejs IVerb, oraz udekorowania jej 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 podane są 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ą wykonywane w ramach jakichś większych całości. Sam ostatnio stworzyłem kilka takich drobnych aplikacji, 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.

Na podstawie powyższego uważam, ż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 *