Jeśli chodzi o rozwój oprogramowania, większość narzędzi dostępnych dla nas to aplikacje wiersza poleceń. Warto również zauważyć, że wiele z narzędzi używanych w wierszu poleceń jest całkiem potężnych w tym co mogą osiągnąć, od trywialnych do uciążliwych. Idąc dalej, można łączyć aplikacje wiersza poleceń, aby połączyć sekwencję pracy w celu osiągnięcia pożądanego rezultatu. Nauka istniejących poleceń wiersza poleceń pozwala zwiększyć produktywność i lepiej zrozumieć, jakie możliwości są pod ręką i jakie zadania możesz potrzebować zaimplementować na własną rękę.
Gdy projektujesz narzędzie wiersza poleceń, naprawdę musisz zwrócić uwagę na to, kto, lub co, jest twoim docelowym odbiorcą. Na przykład, jeśli wszyscy ludzie, którzy będą używać tego narzędzia, używają tego samego systemu operacyjnego i środowiska, wtedy masz największą elastyczność w korzystaniu z pełnego ekosystemu. Jeśli jednak musisz wdrożyć swoją aplikację wiersza poleceń do pracy w wielu systemach operacyjnych, masz teraz ograniczone narzędzia, których możesz użyć do tego, co jest dostępne na wszystkich tych systemach – lub musisz wdrożyć wyjątki dla każdego systemu operacyjnego do swojej aplikacji. (To może być bardzo żmudne do zbadania i przetestowania.)
Rozwiązaniem najczęściej używanym do obsługi różnych środowisk OS jest unikanie wszelkich specyficznych dla danego systemu operacyjnego narzędzi zewnętrznych, gdy jest to możliwe lub delegowanie odpowiedzialności do bibliotek, które już wykonały ciężką pracę przy wdrażaniu funkcji do pracy dla wielu architektur. Im bardziej możesz ograniczyć implementację do jednego języka bez zależności od zewnętrznych zależności, tym łatwiej będzie utrzymać projekt.
Komponenty aplikacji wiersza poleceń w Rubim
Są trzy obszary wymagające uwagi przy tworzeniu aplikacji wiersza poleceń: wejście, wyjście i wszystko pomiędzy. Jeśli tworzysz jednorazowe narzędzie wiersza poleceń, które po prostu pobiera dane wejściowe lub źródło, przetwarza je/formatuje i wypluwa informacje, to Twoja praca jest raczej prosta. Jednak jeśli tworzysz interfejs użytkownika z menu do nawigacji, sprawy zaczynają się komplikować.
Przetwarzanie danych wejściowych
Aby rozpocząć przetwarzanie danych wejściowych, mamy parametry, które mogą być przekazane do twojej aplikacji. Argumenty te są najbardziej typowym sposobem uruchamiania polecenia, ze szczegółami dotyczącymi tego, jak chcesz je wykonać i uniknąć potrzeby korzystania z systemów menu. W tym przykładzie:
ruby -e "puts RUBY_VERSION"
Ruby jest uruchamianym programem wiersza poleceń, -e
jest nazywane flagą, a "puts RUBY_VERSION"
jest wartością podaną dla flagi. W tym przypadku, flaga Rubiego -e
oznacza wykonanie poniższego polecenia jako kodu Rubiego. Kiedy wykonuję powyższe polecenie z linii poleceń, otrzymuję 2.4.0
wypisane na standardowe wyjście (które po prostu pokazuje się w następnej linii).
Argument wejściowy
Argument wejściowy, lub parametry, to wszystkie dodatkowe bity tekstu wprowadzone po poleceniu i oddzielone spacją. Prawie wszystkie polecenia pozwalają na użycie argumentu flagi pomocy. Argument flagi ma przed sobą myślnik lub dwa.
Standardowe flagi pomocy to -h
i --help
. W czasach MSDOS było to (i może nadal być) /?
. Nie wiem czy trend dla flag w Windows utrzymuje się na używaniu forward-slash jako znacznika flagi, ale międzyplatformowe skrypty wiersza poleceń używają myślników.
W Rubim, dane wejściowe z wiersza poleceń są umieszczane w dwóch różnych zmiennych: ARGV i ARGF. ARGV obsługuje parametry wejściowe jako tablicę łańcuchów; ARGF służy do obsługi strumieni danych. Możesz użyć ARGV bezpośrednio dla parametrów, ale może to wymagać więcej pracy niż potrzebujesz. Istnieje kilka bibliotek Rubiego stworzonych do pracy z argumentami wiersza poleceń.
OptionParser
OptionParser ma tę zaletę, że jest dołączony do Rubiego, więc nie jest zewnętrzną zależnością. OptionParser pozwala w prosty sposób zarówno wyświetlić dostępne opcje wiersza poleceń, jak i przetworzyć dane wejściowe na dowolny obiekt Rubiego. Oto fragment jednego z moich narzędzi wiersza poleceń dfm:
require 'optionparser'options = {}printers = Array.newOptionParser.new do |opts| opts.banner = "Usage: dfm \nDefaults: dfm -xd ." + File::SEPARATOR opts.on("-f", "--filters FILTERS", Array, "File extension filters") do |filters| options = filters end opts.on("-x", "--duplicates-hex", "Prints duplicate files by MD5 hexdigest") do |dh| printers << "dh" end opts.on("-d", "--duplicates-name", "Prints duplicate files by file name") do |dh| printers << "dn" end opts.on("-s", "--singles-hex", "Prints non-duplicate files by MD5 hexdigest") do |dh| printers << "sh" end opts.on("-n", "--singles-name", "Prints non-duplicate files by file name") do |dh| printers << "sn" endend.parse!
W tym przykładzie, każdy blok opts.on
zawiera kod do wykonania, jeśli flaga została przekazana w wierszu poleceń. Cztery dolne opcje (w ramach ich bloków) po prostu dołączają informacje o fladze do tablicy, którą mogę później wykorzystać.
Pierwsza z nich ma Array
podane jako jeden z parametrów do on
, więc dane wejściowe dla tej flagi zostaną przekonwertowane na tablicę Ruby i przechowywane w moim haszu o nazwie options
pod kluczem filters
.
Pozostałe parametry podane do metody on
to szczegóły flagi i opis. Zarówno krótka, jak i długa flaga będą działać, aby wykonać kod podany w poniższym bloku.
OptionParser ma również domyślnie wbudowane flagi -h
i --help
, więc nie musisz wymyślać koła na nowo. Po prostu wpisz dfm -h
dla powyższego narzędzia wiersza poleceń, a ono ładnie wypisuje pomocny opis:
Usage: dfm Defaults: dfm -xd ./ -f, --filters FILTERS File extension filters -x, --duplicates-hex Prints duplicate files by MD5 hexdigest -d, --duplicates-name Prints duplicate files by file name -s, --singles-hex Prints non-duplicate files by MD5 hexdigest -n, --singles-name Prints non-duplicate files by file name
Slop
OptionParser jest trochę nieprecyzyjny, jeśli chodzi o wypisywanie definicji wiersza poleceń. Gem Slop został zaprojektowany, aby umożliwić ci napisanie własnego parsowania wiersza poleceń z dużo mniejszym wysiłkiem. Zamiast podawać bloki kodu w definicji flagi, Slop po prostu tworzy obiekt, który możesz odpytywać w swojej aplikacji, aby zobaczyć, czy flaga została podana i jaka wartość(y) mogła być dla niej podana.
# Excerpt from https://github.com/leejarvis/slopopts = Slop.parse do |o| o.string '-h', '--host', 'a hostname' o.integer '--port', 'custom port', default: 80 o.bool '-v', '--verbose', 'enable verbose mode' o.bool '-q', '--quiet', 'suppress output (quiet mode)' o.bool '-c', '--check-ssl-certificate', 'check SSL certificate for host' o.on '--version', 'print the version' do puts Slop::VERSION exit endendARGV #=> -v --host 192.168.0.1 --check-ssl-certificateopts #=> 192.168.0.1opts.verbose? #=> trueopts.quiet? #=> falseopts.check_ssl_certificate? #=> trueopts.to_hash #=> { host: "192.168.0.1", port: 80, verbose: true, quiet: false, check_ssl_certificate: true }
Ta prostota może pomóc uprościć twoją bazę kodu i twój zestaw testów, jak również przyspieszyć czas rozwoju.
Output (STDOUT)
Pisząc proste narzędzie wiersza poleceń, często chcesz, aby wyprowadzało ono wyniki wykonanej pracy. Pamiętając o tym, jaki jest cel tego narzędzia, w dużej mierze określisz, jak chcesz, aby wyglądało wyjście.
Możesz sformatować dane wyjściowe jako prosty ciąg, listę, hash, tablicę zagnieżdżoną, JSON, XML lub inną formę danych do konsumpcji. Jeśli twoje dane mają być przesyłane strumieniowo przez połączenie sieciowe, wtedy będziesz chciał spakować dane w ciasno upakowany ciąg danych. Jeśli dane mają być widoczne dla oczu użytkownika, wtedy będziesz chciał je rozwinąć w prezentowalny sposób.
Wiele istniejących narzędzi wiersza poleceń Linux/Mac może drukować szczegóły w parach lub zestawach wartości. Informacje mogą być oddzielone dwukropkiem, spacjami, tabulatorami i blokami wcięć. Kiedy nie jesteś pewien, jak dokładnie może to być użyte, wybierz najprostszy i najbardziej reprezentatywny sposób prezentacji danych.
Jednym z przykładów celu, który możesz potrzebować rozważyć, jest narzędzie do testowania API. Wiele interfejsów API zapewnia odpowiedź JSON i można do nich uzyskać dostęp za pomocą narzędzia wiersza poleceń, takiego jak curl
. Jeśli przepustowość jest problemem, użyj metody to_json
JSON, ale jeśli jest to przeznaczone do pracy na maszynach lokalnych, użyj pretty_generate
.
x = {"hello" => "world", this: {"apple" => 4, tastes: "delicious"}}require 'json'puts x.to_json# {"hello":"world","this":{"apple":4,"tastes":"delicious"}}puts JSON.pretty_generate( x )# {# "hello": "world",# "this": {# "apple": 4,# "tastes": "delicious"# }# }
Możesz również użyć YAML dla danych.
require 'yaml'puts x.to_yaml# ---# hello: world# :this:# apple: 4# :tastes: delicious
Jeśli chcesz mieć bardziej złożone dane wyjściowe ładnie sformatowane, to gorąco polecam użycie klejnotu awesome_print. To da ci szczególną kontrolę nad twoją prezentacją na wyjściu.
Standardowy błąd (STDERR)
To jest inny rodzaj wyjścia, które może wystąpić z aplikacji wiersza poleceń. Kiedy coś jest nie tak lub poszło nie tak, zwyczajowo narzędzie wiersza poleceń pisze na wyjście znane jako STDERR. Pojawi się ono jako zwykłe wyjście, ale inne narzędzia mogą zweryfikować, że polecenie nie powiodło się.
STDERR.puts "Oops! You broke it!"
Najczęstszą praktyką jest używanie loggera do zapisywania szczegółowych informacji o błędach. Możesz skierować to do wyjścia STDERR w wierszu poleceń lub ewentualnie do pliku dziennika.
Interfejs użytkownika (STDIN i STDOUT)
Pisanie interfejsu użytkownika może być jedną z najbardziej satysfakcjonujących rzeczy do zrobienia. Pozwala zastosować trochę artystycznego designu i zapewnić różne sposoby interakcji dla użytkownika.
Minimalny interfejs użytkownika to wyświetlanie tekstu z monitem oczekującym na wprowadzenie danych. Może on wyglądać tak prosto jak:
Who is your favorite super hero /Batman/Wonder-Woman ?
Powyżej znajduje się pytanie, opcje oraz opcja domyślna, jeśli użytkownik zdecyduje się nacisnąć klawisz enter bez wpisywania czegokolwiek. W Rubim, wyglądałoby to następująco:
favorite = "Superman"printf "Who is your favorite hero /Batman/Wonder Woman?"input = gets.chompfavorite = input unless input.empty?
Jest to bardzo prymitywny kod dla wejścia i wyjścia, i jeśli zamierzasz pisać UI, gorąco polecam wypróbowanie gemów takich jak highline lub tty.
Z klejnotem highline, mógłbyś napisać powyższe jako:
require 'highline/import'favorite = ask("Who is your favorite hero Superman/Batman/Wonder Woman?") {|question| question.in = question.default = "Superman"}
Tutaj, highline pokaże pytanie, pokaże domyślne, i złapie wszelkie nieprawidłowe opcje, powiadamiając użytkownika, że nie wybrał jednej z podanych opcji.
Zarówno highline jak i tty mają ogromną ilość dodatkowych rzeczy, które możesz zrobić dla menu i doświadczenia użytkownika, z takimi drobiazgami jak dodawanie kolorów do twojego wyświetlacza. Ale znowu musisz wziąć pod uwagę, kto jest twoim docelowym odbiorcą.
Im więcej wizualnego doświadczenia zapewniasz, tym bardziej musisz zwrócić uwagę na międzyplatformowe możliwości tych narzędzi. Nie wszystkie linie poleceń obsługują te same dane prezentacji w ten sam sposób, co powoduje złe doświadczenia użytkownika.
Kompatybilność międzyplatformowa
Wspaniałą wiadomością jest to, że Ruby posiada wiele rozwiązań potrzebnych do tworzenia narzędzi dla różnych systemów operacyjnych. Kiedy Ruby jest kompilowany dla konkretnego systemu operacyjnego, plik źródłowy dla RbConfig jest generowany z wartościami bezwzględnymi natywnymi dla systemu, na którym został zbudowany, zapisanymi w formacie hash. Jest to zatem klucz do wykrywania i używania cech systemu operacyjnego.
Aby zobaczyć ten plik w swoim ulubionym edytorze tekstu, możesz uruchomić następujący kod Rubiego:
editor = "sublime" # your preferred editor hereexec "#{editor} #{RbConfig.method(:ruby).source_location}"
Pokaże ci to wszystko w przystępny sposób, co moim zdaniem jest lepsze niż oglądanie hasha przez RbConfig::CONFIG
. Ten hash zawiera przydatne rzeczy jak komendy używane do obsługi systemu plików, wersję Rubiego, architekturę systemu, gdzie znajduje się wszystko co ważne dla Rubiego i inne tego typu rzeczy.
Aby sprawdzić system operacyjny, możesz zrobić:
case RbConfig::CONFIGwhen /mingw32|mswin/ # Probably a Windows operating systemelse # Most likely a Unix compatible system like BSD/Mac/Linuxend
Aby uzyskać inne wartości specyficzne dla systemu operacyjnego, możesz zajrzeć do kodu źródłowego Gem::Platform.
Teraz, specyficzne dla systemu plików polecenia przechowywane tutaj nie są przeznaczone do użycia z tego zasobu. Ruby posiada klasy Dir, File i Pathname napisane do tego celu.
Gdy potrzebujesz wiedzieć czy plik wykonywalny istnieje w systemowej PATH, wtedy będziesz chciał użyć MakeMakefile.find_executable. Ruby obsługuje budowanie rozszerzeń C i jedną z fajnych funkcji, które do tego dodali jest możliwość sprawdzenia czy istnieje plik wykonywalny do wywołania.
Ale to wygeneruje plik logu w bieżącym katalogu twojego systemu za każdym razem, gdy go uruchomisz. Tak więc, aby uniknąć pisania tego pliku dziennika, będziesz musiał wykonać następujące czynności:
require 'mkmf'MakeMakefile::Logging.instance_variable_set(:@log, File.open(File::NULL, 'w'))executable = MakeMakefile.find_executable('clear') # or whatever executable you're looking for
Gdy rysujesz wizualne menu okienkowe w wierszu poleceń, preferowane jest, aby menu pozostawało w stałej pozycji, gdy wyświetlacz się aktualizuje. Najprostszym sposobem osiągnięcia tego jest wyczyszczenie wiersza poleceń między każdym zapisem pełnego wyświetlacza.
W powyższym przykładzie szukałem polecenia clear
. W systemie Windows polecenie powłoki do czyszczenia wiersza poleceń to cls
, ale nie będziesz w stanie znaleźć dla niego pliku wykonywalnego, ponieważ jest to część wewnętrznego kodu command.com
.
Dobrą wiadomością jest to, że przechwycenie łańcucha wyjściowego z linuksowego polecenia clear
daje taką sekwencję kodu ucieczki: \e[3J\e[H\e[2J
. Ja i inni testowaliśmy to w systemach Windows, Mac i Linux, i robi to dokładnie to, co chcemy: czyści ekran do ponownego rysowania na nim.
Ten ciąg ucieczki ma trzy różne działania, które wykonuje:
CLEAR = (ERASE_SCOLLBACK = "\e[3J") + (CURSOR_HOME = "\e[H") + (ERASE_DISPLAY = "\e[2J")
Jednakże, lepiej jest używać biblioteki wiersza poleceń niż pisać kody ucieczki samodzielnie. Ale ten jest warty wspomnienia.
Testowanie STDIN/STDOUT/STDERR
W przypadku wszystkiego, co zapisuje do czegoś takiego jak STDOUT lub STDERR, najrozsądniej byłoby utworzyć wewnętrzną zmienną w obiekcie UI Ruby, która może być zmieniona za pomocą opcjonalnego parametru do new
. Więc kiedy twój program normalnie działa, wartość będzie STDOUT
. Ale kiedy będziesz pisał testy, przekażesz StringIO.new
, którą będziesz mógł łatwo przetestować.
Gdy próbujesz odczytać IO z polecenia wykonanego poza Rubim, testowanie jest nieco bardziej skomplikowane. Prawdopodobnie będziesz musiał zajrzeć do Open3.popen3 aby obsłużyć każdy strumień IO STDIN, STDOUT, oraz STDERR.
Używanie komend Bash
Bash zawitał do Windows (WSL)! Oznacza to, że masz dostęp do poleceń basha w ogromnej większości systemów operacyjnych używanych na świecie. To otwiera więcej możliwości dla twojego własnego narzędzia wiersza poleceń.
To typowe dla „potokowania” poleceń między sobą, wysyłając wyjście jednego polecenia jako strumień do następnego. Wiedząc o tym, możesz rozważyć dodanie obsługi strumieniowego przesyłania danych wejściowych lub zastanowić się, jak lepiej sformatować dane wyjściowe dla innych narzędzi wiersza poleceń.
Oto przykład podstawiania regex za pomocą polecenia sed
Basha, otrzymującego dane wejściowe przesyłane przez echo:
echo hello | sed "s/ll/ck n/g"
To daje heck no
. Im więcej nauczysz się o Bashu, tym lepiej będziesz mógł załatwiać sprawy w wierszu poleceń i tym lepiej będziesz przygotowany do pisania własnych, lepszych narzędzi wiersza poleceń.
Podsumowanie
Jest wiele do nauczenia się, jeśli chodzi o pisanie własnych narzędzi wiersza poleceń. Im więcej sytuacji musisz uwzględnić, tym bardziej złożone stają się rzeczy.
Poświęć czas na naukę narzędzi, które masz pod ręką, a zbierzesz wiele nagród, o których istnieniu nie wiedziałeś. Życzę ci wszystkiego najlepszego!