Stworzyłem bibliotekę rozszerzającą obsługę konsoli w .Net. Zobacz jak to zrobiłem

Jeśli nie programujesz aplikacji konsolowych to taka biblioteka do niczego Ci się nie przyda. Jeśli sporadycznie piszesz niewielkie narzędzia, które wyświetlają coś na ekranie to już możesz rozważyć wykorzystanie wybranych komponentów, by usprawnić tworzenie swojego kawałka oprogramowania i ewentualnie sprawić, że będzie lepiej wyglądał. Największą korzyść odniosą jednak twórcy dwóch rodzajów oprogramowania: gier w trybie tekstowym i zaawansowanych narzędzi, złożonych z licznych, skomplikowanych komend lub prezentujących większe ilości danych, np. w formie tabelarycznej.

Tabele

Tabele to zasadniczo takie bardziej skomplikowane ramki. Pomijając, oczywiście, tabelę „bezramkową”. Niestety jest to też chyba najbardziej zaniedbana, jeszcze, część biblioteki. API wymaga porządkowania i standaryzacji.

Konfiguracja styli została zrealizowana w podobny sposób jak przy ramkach – są kolory (chociaż więcej, bo dochodzą kolory nagłówka, a tekst może mieć odmiennie kolorowane linie parzyste i nieparzyste) a sama definicja ramki jest dłuższa (i różna, w zależności od jej typu). Sugerowałbym za to nie podawać znaku wypełnienia – tabelki są bardziej czytelne, gdy puste przestrzenie pozostają… puste.

Możliwe jest także zażądanie by tabelka zwijała zawartość komórek przekraczającą ich wyliczoną długość. Ale z tego co pamiętam to jeszcze nie działa idealnie, zwłaszcza z kwestiami spacji czy znaków interpunkcyjnych. Tymczasem przykład tabelki wyświetlanej z ramką lub bez, ale z zawartością, która się bezpiecznie mieści na ekranie.

Oprócz stylu, który może być odrobinę różny w zależności od rodzaju tabelki, należy utworzyć definicje kolumn tabeli:

Opcjonalnie, można ustawić kolumnom żądaną szerokość. Jeśli tego nie zrobimy algorytm postara się przyjąć optymalne szerokości zarówno do pomieszczenia nagłówków, jak i zawartości wierszy. W przypadku, gdy nie jest to możliwe zacznie przydzielać dostępną powierzchnię proporcjonalnie do wielkości zawartości. Ewentualnie przycinać zbyt długie teksty, jeśli takie są ustawienia. Nie działa to idealnie – zbyt wiele tu warunków brzegowych, zwłaszcza gdy mamy pomieszane w definicji szerokości ustalone i dynamiczne totalnie niedopasowane do zawartości, ale zazwyczaj jest OK.

Co gorsza, gdzieś mi zaginęły demonstracje z tabelką z pełnymi ramkami, póki co jest tylko ta na sposób SpecFlow / Cucumber i „goła”. Przykład z danymi wyplutymi z generatora:

Na pewno chciałbym trochę zmienić definicje kolumn, by np. umożliwić zachowanie niektórych kolumn jako np. never-overflow-resize-to-content np. dla kolumn z liczbami czy datami. Plus może także tabelki, które zamiast liter nieparzystych inaczej kolorują tło? Albo dodają ramki także pomiędzy wierszami? No, na pewno będzie co robić…

Console.Commands

Ta biblioteka funkcjonalnie jest niemal domknięta. Tam, gdzie — moim zdaniem — najbardziej jeszcze wymaga poprawek to i tak raczej kwestie współpracy z interfejsem, które są do naprawienia w bibliotece Operations. I ewentualne rozszerzanie / uzupełnianie drugorzędowej funkcjonalności – np. wyświetlanie w pomocy większych ilości tekstu i informacji, formatowania, przykładów czy autouzupełniania dla wybranych parametrów itp. Ale do rzeczy.

Najważniejsze założenia, które przyjąłem projektując tę bibliotekę były następujące:

  • Obsługa różnych stylów formatowania poleceń,
  • Ściśle typowane parametry polecenia, z automatycznymi konwersjami i większością walidacji załatwianą przez framework,
  • Łatwość rozszerzania i konfigurowania rozmaitych aspektów,
  • Automatyczne generowanie ekranów pomocy na podstawie definicji,
  • Przechowywanie i przekazywanie maszyny stanu pomiędzy poleceniami,
  • Obsługę zarówno parametrów nazwanych, jak i „swobodnych”, zarówno w środku, jak i na tylko na końcu polecenia,
  • Auto-uzupełnianie – zarówno przełączników, samych komend, jak i wybranych wartości (to ostatnio sterowane już przez samą komendę),
  • Możliwość uruchamiania aplikacji „hostującej” zarówno jako pojedynczej komendy, jak i jako silnika komend (wraz z dostępem do historii, schowka i autouzupełniania).

Większość z tych założeń udało się już zrealizować w zadowalającym stopniu!

Omówię teraz przykładowe polecenie z dema. Po więcej zachęcam – ponownie — do przeglądania tutoriali, aplikacji demonstracyjnych oraz unit testów.

Model

Każda komenda składa się z dwóch „części”: definicji parametrów oraz klasy wykonawczej. Obie łączy się ze sobą za pomocą dedykowanych atrybutów. Model:

Model składa się z prostego zestawu właściwości, opatrzonych dwoma rodzajami atrybutów: atrybuty dla parsera i silnika informujące o zachowaniu i rodzaju parametru oraz atrybuty do generowania treści na ekranach pomocy. Niektóre, jak np. CommandName, CommandOptionFlag czy CommandOptionName będą wykorzystane w obu zakresach. Po zażądaniu w linii poleceń pomocy dla tej komendy możemy zobaczyć coś takiego:

Ale też i np. coś takiego:

Wszystko zależy od tego, jakie na początku przyjęliśmy ustawienia formatowania / parsowania. Takie:

Lub np. takie:

Widać też od razu, jaki ma to wpływ też na samą aplikację – po sposobie żądania pomocy. Zresztą, sam silnik o tym informuje stosownymi komunikatami:

Jeśli chodzi o możliwe wartości parametrów typu opcja to są to flagi (bool), enumeracje lub typy, dla których istnieją zdefiniowane konwertery / parsery (w praktyce: liczby, daty, Guidy) albo po prostu łańcuchy znaków.

Przykładowy parser:

Ech. Chyba widzę przestrzeń do poprawy – lepszy byłby generyczny interfejs zamiast abstrakcyjnej klasy bazowej. Ale były ku temu powody, kiedy to pisałem – ten komentarz wiele wyjaśnia 😉:

ZOBACZ TEŻ:  Bootcamp jak nauka jazdy. Po kursie nie zostaniesz kierowcą rajdowym

Egzekutor

Po poprawnym sparsowaniu przekazanej komendy, obiekt modelu wraz z ustawionymi parametrami jest przekazywany do klasy wykonawczej. Ta może się skupić wyłącznie na analizie gotowych parametrów i właściwym wykonaniu żądanych działań:

Nie pamiętam już dokładnie jakie problemy z dziedziczeniem generycznych typów – w końcu od powstania tego kodu minęły niemal dwa lata, ale pewnie przyjrzę się temu jeszcze w najbliższym czasie… Może to była kwestia po prostu skomplikowanego wyszukiwania i konstruowania?

Ważne uwagi. Jak widać klasa wykonawcza sama nie przechowuje w sobie stanu. Nie powinna tego robić. Nie ma żadnej gwarancji, czy będzie tworzona jedna na cały czas życia programu czy od nowa przy każdym poleceniu. Do przechowywania stanu służy obiekt runtimeModel, którym steruje sama aplikacja – typ obiektu jest definiowany przy starcie silnika, a manipulacją zajmują się wyłącznie komendy.

Podobnie, komenda nie ma bezpośredniego dostępu do konsoli – wyświetla wyniki wyłącznie za pomocą szeregu metod dostępnych w interfejsie ICommandOutput. Z jednej strony to był niezły pomysł, ale przeczuwam, że w przyszłości będę chciał to zmienić. Chciałbym uniknąć możliwości wywoływania niepoprawnych operacji przez komendy (jak pisanie poza buforem ekranowym) oraz tworzenia komend interaktywnych (od czegoś w końcu te parametry komendy są!), ale z drugiej – nie chciałbym ograniczać użytkowników biblioteki. Do przemyślenia jak to sensownie pogodzić.

Konfiguracja

Silnik oczekuje rozmaitych parametrów. Dla niektórych ma dostarczone implementacje domyślne, niektóre należy dostarczyć. Ze względu na potencjalną złożoność tej inicjalizacji postanowiłem wykorzystać wzorzec fluent-builder.

Co można / trzeba przekazać?

  • Oczywiście konsolę. Gdzieś trzeba wyświetlać wyniki, komunikaty i pobierać dane od użytkownika.
  • Style. Dużo styli. W tej chwili chyba nawet za dużo. Po uproszczeniu sposobu komunikowania się klas wykonawczych z ekranem na pewno zostaną style używane do wyświetlania ekranów pomocy, błędów, ostrzeżeń i samej linii poleceń. Marzy mi się też linia poleceń, która będzie kolorowała składnię…
  • Dependecy Incjection Resolver’a. Jeśli obiekty wykonawcze muszą mieć wstrzykiwane jakieś referencje – to koniecznie. Bez tego, domyślnie, muszą mieć bezparametrowe konstruktory.
  • Lista komend / bibliotek z komendami. Albo i jedno, i drugie.
  • Lista konwerterów opcjonalnych (todo).
  • Obiekt stanu / kontekstu (Ten jest co prawda podawany dopiero na wejściu metody Run).
  • Obiekt do komunikacji z systemowych schowkiem do operacji kopiuj-wklej.

W przykładowej demonstracji są dość proste implementacje komend DIR i CD, a obiekt stanu zawiera jedynie bieżący katalog. Obserwuję też konkurencyjne rozwiązanie, które znalazłem kilka miesięcy temu. W niektórych aspektach jest moim zdaniem zbyt skomplikowane (nie cierpię podejścia Convention-over-Configuration – zbyt wile w nim magii) i niektóre funkcje, które są u mnie (od początku 😉) tam ciągle nie działają, ale w niektórych aspektach jest wykonane lepiej / prościej / czytelniej… Więc wkrótce refaktoring 😉

Przyszłość

Sporo. Pomysłów mam wiele. Oto najważniejsze.

  • Autouzupełnianie dla enumów lub wybranych komend.
  • Kolorowanie składni w wierszu polecenia.
  • Naprawienie wiersza polecenia.
  • Rozszerzona pomoc – dodatkowe opisy, przykłady itp. Plus oczywiście składnia definicji.
  • Usprawnienia ekranu pomocy.
  • Lokalizacja komunikatów i ekranów pomocy.
  • Lepsze sposoby wyświetlania wyników / komunikatów przez komendy, ale nadal bezpieczne… (Z tym czekam jeszcze na stabilizację pozostałych projektów i ich API).
  • Usprawnienie hierarchii dziedziczenia i generyków.
  • Walidacja spójności definicji i komend przy starcie (np. czy wszystkie typy danych mają konwertery, czy opcjonalne interfejsy wymagane przez definicję komendy są przez nią dostarczone itd.).

Podsumowanie

Z kodem do ludzi! Do tej pory pracowałem nad biblioteką głównie dla siebie, pod siebie i pod moje potrzeby. Wychodząc z nią między ludzi spodziewam się, że niektóre rozwiązania mogą być złe / niewygodne / zbyt ograniczone itd. Albo, że zupełnie czegoś nie przewidziałem, nie przetestowałem, nie wymyśliłem…

Jeśli znajdujesz w swojej pracy lub hobby miejsce na wykorzystanie moje biblioteki to świetna wiadomość. Jeśli jednak coś Ci w niej przeszkadza – to nie pozostawiaj tego w czterech ścianach swojej pracowni – daj znać, dołącz do projektu, zgłoś buga! Ja już podczas pisania tego artykułu, najwyższym wysiłkiem woli, powstrzymywałem się od natychmiastowego rozwiązywania zauważonych problemów w samym kodzie – inaczej nigdy nie skończyłbym pisać tego tekstu. Pomóż mi stworzyć narzędzie, które będzie pomocą dla nas obu, a nie drogą przez mękę. Co kilka (mądrych) głów to nie jedna.

Może z czasem uderzymy też na .Net Core?

Zapraszamy do dyskusji

Patronujemy

 
 
More Stories
Zdjęcia kału w służbie AI? Ciekawa akcja firmy Seed