A szoftverfejlesztés során a rendelkezésünkre álló eszközök többsége parancssori alkalmazás. Érdemes azt is megjegyezni, hogy a parancssoron használt eszközök közül sokan elég erősek abban, amit el tudnak végezni, a triviális dolgoktól a fárasztóig. Ha ezt továbbvisszük, a parancssoros alkalmazásokat úgy kombinálhatjuk, hogy a kívánt kimenet eléréséhez egy munkasorozatot láncoljunk össze. A meglévő parancssori parancsok elsajátítása lehetővé teszi, hogy növelje a termelékenységét, és jobban megértse, milyen képességek állnak rendelkezésére, és milyen feladatokat kell esetleg egyedül megvalósítania.
A parancssori eszköz tervezésekor valóban figyelnie kell arra, hogy ki vagy mi a célközönsége. Ha például az összes ember, aki ezt az eszközt használni fogja, ugyanazt az operációs rendszert és környezetet használja, akkor a legnagyobb rugalmassággal a teljes ökoszisztémát használhatja ki. Ha azonban a parancssoros alkalmazást úgy kell megvalósítania, hogy az több operációs rendszerben is működjön, akkor a használható eszközök körét most arra korlátozta, ami az összes ilyen rendszeren elérhető — vagy az egyes operációs rendszerekre vonatkozó kivételeket kell implementálnia az alkalmazásába. (Ez nagyon fárasztó lehet kutatni és tesztelni.)
A leggyakrabban használt megoldás a különböző operációs rendszerek környezetének kezelésére az, hogy lehetőség szerint kerüljük az operációs rendszer-specifikus külső eszközöket, vagy a felelősséget olyan könyvtárakra bízzuk, amelyek már elvégezték a nehéz munkát a funkciók több architektúrára való implementálásával. Minél inkább le tudja szorítani a megvalósítást egy alapnyelvre anélkül, hogy külső függőségektől függne, annál könnyebb lesz karbantartani a projektet.
A parancssoros alkalmazás összetevői Ruby-ban
Három területet kell kezelni egy parancssoros alkalmazás létrehozásakor: a bemenetet, a kimenetet és mindent, ami a kettő között van. Ha egy egyszeri parancssoros eszközt fejlesztesz, amely egyszerűen fogadja a bemenetet vagy a forrást, feldolgozza/formázza és kiköpi az információt, akkor a munkád meglehetősen egyszerű. Ha azonban egy felhasználói felületet fejlesztesz, ahol menükben kell navigálnod, a dolgok kezdenek bonyolultabbá válni.
Bemenet feldolgozása
A bemenet feldolgozásának megkezdéséhez vannak paramétereink, amelyeket megadhatunk az alkalmazásodnak. Ezek az argumentumok a legjellemzőbb módja a parancs indításának, részletesen megadva, hogyan szeretnénk, hogy a parancs végrehajtásra kerüljön, és elkerüljük a menürendszerek szükségességét. Ebben a példában:
ruby -e "puts RUBY_VERSION"
A Ruby a futtatandó parancssori program, -e
a flag, "puts RUBY_VERSION"
pedig a flag számára megadott érték. Ebben az esetben a Ruby -e
flag azt jelenti, hogy a következőket hajtja végre Ruby kódként. Ha a parancssoron futtatom a fentieket, akkor a standard kimenetre kiírt 2.4.0
kapom vissza (ami egyszerűen a következő sorban jelenik meg).
Argument bemenet
Az argumentum bemenet, vagy paraméterek, a parancs után beírt és szóközzel elválasztott összes extra szövegdarab. Majdnem minden parancs lehetővé teszi a help flag argumentumot. A flag argumentum előtt egy vagy két kötőjel áll.
A standard help flagek a -h
, és a --help
. Az MSDOS idején ez volt (és talán még mindig az) /?
. Nem tudom, hogy a Windowsban a zászlókra vonatkozó trend megmaradt-e az előrevágott kötőjel használatánál, de a platformokon átívelő parancssori szkriptek manapság már kötőjelet használnak.
A Rubyban a parancssorból érkező bemenet két különböző változóba kerül: ARGV és ARGF. Az ARGV a bemeneti paramétereket karakterláncok tömbjeként kezeli; az ARGF az adatfolyamok kezelésére szolgál. Az ARGV-t használhatod közvetlenül a paraméterekhez, de ez több munkát jelenthet, mint amennyire szükséged van. Van néhány Ruby könyvtár, amely a parancssori argumentumokkal való munkára épült.
OptionParser
Az OptionParser előnye, hogy a Ruby tartalmazza, tehát nem külső függőség. Az OptionParser egyszerű módot biztosít mind a parancssori opciók megjelenítésére, mind a bemenet bármilyen Ruby objektummá történő feldolgozására. Íme egy részlet az egyik parancssori eszközömből 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!
Ebben a példában minden opts.on
blokkban van kód, amelyet akkor kell végrehajtani, ha a parancssorban átadtuk a zászlót. Az alsó négy opció (a saját blokkjukon belül) egyszerűen a zászló információját egy tömbbe csatolja, hogy később felhasználhassam.
Az elsőnél a Array
van megadva a on
egyik paramétereként, így ennek a zászlónak a bemenete egy Ruby tömbbe lesz konvertálva és a options
nevű hash-omban tárolva a filters
kulcs alatt.
A on
metódusnak megadott többi paraméter a zászló adatai és a leírás. Mind a rövid, mind a hosszú zászlók működnek a következő blokkban megadott kód végrehajtásához.
AzOptionParser alapértelmezés szerint rendelkezik a -h
és --help
zászlókkal is, így nem kell újra feltalálni a kereket. Egyszerűen írja be a dfm -h
-t a fenti parancssori eszközhöz, és az szépen kiad egy hasznos leírást:
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
AzOptionParser egy kicsit bőbeszédű, amikor a parancssori definíciók kiírásáról van szó. A Slop nevű drágakő arra szolgál, hogy sokkal kevesebb erőfeszítéssel megírhassuk a saját parancssori elemzőinket. Ahelyett, hogy a flag definícióban szereplő kódblokkokat kellene megadnod, a Slop egyszerűen létrehoz egy objektumot, amelyet lekérdezhetsz az alkalmazásodban, hogy megtudd, megadtál-e egy flaget, és milyen érték(ek)et adtál meg hozzá.
# 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 }
Ez az egyszerűség segíthet egyszerűsíteni a kódbázisát és a tesztcsomagját, valamint felgyorsíthatja a fejlesztési időt.
Kimenet (STDOUT)
Az egyszerű parancssoros eszköz írása során gyakran szeretnénk, ha az elvégzett munka eredményét kimenne. Ha szem előtt tartja, hogy mi az eszköz célja, az nagymértékben meghatározza, hogy a kimenet hogyan nézzen ki.
A kimeneti adatokat formázhatja egyszerű karakterlánc, lista, hash, beágyazott tömb, JSON, XML vagy más fogyasztandó adatformátum formájában. Ha az adatokat hálózati kapcsolaton keresztül fogod streamelni, akkor az adatokat egy szorosan csomagolt adatsorba kell csomagolnod. Ha a felhasználó szemének szánja, akkor szalonképes módon akarja kibontani.
Néhány létező Linux/Mac parancssoros eszköz képes az adatokat párban vagy értékkészletekben kiírni. Az információkat kettősponttal, szóközökkel, tabulátorokkal és behúzási blokkokkal lehet elválasztani. Ha nem vagy biztos abban, hogy pontosan hogyan lehet felhasználni, válaszd az adatok bemutatásának legegyszerűbb és legprezentatívabb módját.
Egy példa a célpontra, amit érdemes megfontolni, egy API-tesztelő eszköz. Sok API ad JSON választ, és egy olyan parancssori eszközzel érhető el, mint a curl
. Ha a sávszélesség gondot jelent, akkor használd a JSON to_json
módszerét, de ha helyi gépi munkára szánod, akkor használd a pretty_generate
-t.
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"# }# }
Hasonlóképpen használhatod a YAML-t az adatokhoz.
require 'yaml'puts x.to_yaml# ---# hello: world# :this:# apple: 4# :tastes: delicious
Ha bonyolultabb kimenetet szeretnél szépen formázva, akkor nagyon ajánlom az awesome_print gem használatát. Ez lehetővé teszi a kimeneten történő megjelenítés egyedi vezérlését.
Standard hiba (STDERR)
Ez egy másik fajta kimenet, amely a parancssori alkalmazásokból adódhat. Ha valami nem stimmel, vagy rosszul sikerült, akkor szokás, hogy a parancssori eszköz az STDERR néven ismert kimenetre ír. Ez normál kimenetként jelenik meg, de más eszközök ellenőrizhetik, hogy a parancs nem volt sikeres.
STDERR.puts "Oops! You broke it!"
Az általánosabb gyakorlat az, hogy egy Logger segítségével konkrét részleteket írunk a hibákról. Ezt átirányíthatja a parancssor STDERR kimenetére vagy esetleg egy naplófájlba.
Kezelőfelület (STDIN és STDOUT)
A felhasználói felület megírása az egyik leghálásabb dolog lehet. Lehetővé teszi, hogy némi művészi designt alkalmazzunk, és különböző módokat biztosítsunk a felhasználó számára az interakcióra.
A minimális felhasználói felület egy szöveg megjelenítése egy bevitelre váró prompttal. Ez nézhet ki olyan egyszerűen, mint:
Who is your favorite super hero /Batman/Wonder-Woman ?
A fentiekben szerepel a kérdés, a lehetőségek és egy alapértelmezett opció, amennyiben a felhasználó úgy dönt, hogy az enter billentyűt nyomja meg anélkül, hogy bármit is beírna. Sima Ruby-ban ez így nézne ki:
favorite = "Superman"printf "Who is your favorite hero /Batman/Wonder Woman?"input = gets.chompfavorite = input unless input.empty?
Ez nagyon durva kód a be- és kimenethez, és ha felhasználói felületet akarsz írni, erősen ajánlom, hogy próbálj ki egy olyan drágakövet, mint a highline vagy a tty.
A highline gemmel a fentieket így írhatod:
require 'highline/import'favorite = ask("Who is your favorite hero Superman/Batman/Wonder Woman?") {|question| question.in = question.default = "Superman"}
Itt a highline megjeleníti a kérdést, megmutatja az alapértelmezettet, és elkapja a hibásan beírt opciókat, értesítve a felhasználót, hogy nem választotta ki a megadott lehetőségek egyikét sem.
A highline és a tty is rengeteg további dolgot tudsz tenni a menü és a felhasználói élmény érdekében, olyan finomságokkal, mint a színek hozzáadása a kijelzőhöz. De ismét figyelembe kell venned, hogy ki a célközönséged.
Minél több vizuális élményt nyújtasz, annál jobban oda kell figyelned ezeknek az eszközöknek a platformokon átívelő képességeire. Nem minden parancssor kezeli ugyanúgy ugyanazokat a megjelenítési adatokat, ami rossz felhasználói élményt eredményez.
Keresztplatformos kompatibilitás
A jó hír, hogy a Ruby sok olyan megoldást kínál, ami a különböző operációs rendszerek közötti eszközhasználathoz szükséges. Amikor a Ruby-t lefordítjuk egy adott operációs rendszerre, az RbConfig forrásfájlja az adott rendszerre natív abszolút értékekkel generálódik, hash formátumban elmentve. Ezért ez a kulcs az operációs rendszer jellemzőinek felismeréséhez és használatához.
Hogy ezt a fájlt a kedvenc szövegszerkesztőddel megnézd, a következő Ruby kódot futtathatod:
editor = "sublime" # your preferred editor hereexec "#{editor} #{RbConfig.method(:ruby).source_location}"
Ez mindent szalonképes formában mutat, amit jobbnak érzek, mintha a RbConfig::CONFIG
-en keresztül látnád a hash-t. Ez a hash olyan hasznos dolgokat tartalmaz, mint a fájlrendszer kezeléséhez használt parancsok, a Ruby verziója, a rendszer architektúrája, ahol minden Ruby számára fontos dolog található, és más hasonló dolgok.
Az operációs rendszer ellenőrzéséhez a következőket teheted:
case RbConfig::CONFIGwhen /mingw32|mswin/ # Probably a Windows operating systemelse # Most likely a Unix compatible system like BSD/Mac/Linuxend
A többi operációs rendszer-specifikus értékekért megnézheted a Gem::Platform forráskódját.
Az itt tárolt fájlrendszer-specifikus parancsokat nem ebből az erőforrásból kell használni. A Ruby-ban erre a Dir, File és Pathname osztályok vannak megírva.
Ha tudni akarjuk, hogy egy futtatható fájl létezik-e a rendszer PATH-jában, akkor a MakeMakefile.find_executable parancsot kell használni. A Ruby támogatja a C kiterjesztések építését, és az egyik szép funkció, amit ehhez adtak hozzá, ez a képesség, hogy kiderítse, létezik-e a hívandó futtatható állomány.
De ez minden egyes futtatáskor egy naplófájlt fog generálni a rendszer aktuális könyvtárában. Tehát ahhoz, hogy elkerüld ennek a naplófájlnak az írását, a következőket kell tenned:
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
Ha egy vizuális ablakos menüt rajzolsz a parancssorban, akkor előnyös, ha a menü a kijelző frissítésekor fix pozícióban marad. Ennek legegyszerűbb módja, ha a parancssort a teljes kijelző minden írása között töröljük.
A fenti példában a clear
parancsot kerestem. Windowson a parancssor törlésére szolgáló shell parancs a cls
, de nem fogsz találni hozzá futtathatót, mert ez a command.com
belső kódjának része.
A jó hír az, hogy a Linux clear
parancs string kimenetének megragadása ezt az escape kódsorozatot eredményezi: \e[3J\e[H\e[2J
. Én és mások is teszteltük ezt Windowson, Macen és Linuxon, és ez pontosan azt teszi, amit akarunk: kitakarítja a képernyőt, hogy újra rajzolhassunk rajta.
Ez az escape karakterlánc három különböző műveletet hajt végre:
CLEAR = (ERASE_SCOLLBACK = "\e[3J") + (CURSOR_HOME = "\e[H") + (ERASE_DISPLAY = "\e[2J")
Az escape kódok saját megírása helyett azonban jobb, ha parancssori könyvtárat használunk. De ezt érdemes megemlíteni.
Tesztelés STDIN/STDOUT/STDERR
Minden olyan dolognál, ami valami olyanba ír, mint az STDOUT vagy az STDERR, a legbölcsebb az lenne, ha létrehoznál egy belső változót az UI Ruby objektumodban, amit egy opcionális paraméterrel lehet megváltoztatni new
. Így amikor a programod normálisan fut, az érték STDOUT
lesz. De amikor teszteket írsz, akkor átadod a StringIO.new
értéket, ami ellen könnyen tesztelhetsz.
Ha egy Ruby-n kívül végrehajtott parancsból próbálod kiolvasni az IO-t, akkor a tesztelés egy kicsit bonyolultabb. Valószínűleg meg kell nézned az Open3.popen3-t, hogy kezelje az egyes IO folyamokat STDIN, STDOUT és STDERR.
Bash parancsok használata
A Bash eljött a Windowsra (WSL)! Ez azt jelenti, hogy a világszerte használt operációs rendszerek túlnyomó többségében hozzáférhet a bash-parancsokhoz. Ez több lehetőséget nyit meg a saját parancssori eszköze számára.
Jellemző, hogy a parancsokat “átcsövezzük” egymáson, az egyik parancs kimenetét folyamként elküldve a következőnek. Ennek ismeretében érdemes megfontolni a streaming bemenet támogatását, vagy elgondolkodni azon, hogyan formázhatnánk jobban a kimenetünket más parancssori eszközök fogyasztására.
Itt egy példa egy regex behelyettesítésre a Bash sed
parancsával, amely az echo-tól pipázva kapja a bemenetet:
echo hello | sed "s/ll/ck n/g"
Ez a kimenet heck no
. Minél többet tanulsz a Bashről, annál jobban képessé válsz arra, hogy parancssoron végezd el a dolgokat, és annál jobban fel leszel szerelve arra, hogy jobb saját parancssori eszközöket írj.
Összefoglaló
A saját parancssori eszközeid írásához sok tanulnivaló van. Minél több helyzetet kell figyelembe vennie, annál összetettebbé válnak a dolgok.
Tegyen időt arra, hogy megtanulja a rendelkezésére álló eszközöket, és sok olyan gyümölcsöt fog aratni, amiről nem is tudta, hogy hiányzik. Sok sikert kívánok!