Wanneer het op software-ontwikkeling aankomt, bestaat het merendeel van de tools die we tot onze beschikking hebben uit opdrachtregel-applicaties. Het is ook de moeite waard om op te merken dat veel van de gereedschappen die op de opdrachtregel worden gebruikt, behoorlijk krachtig zijn in wat ze kunnen bereiken, van triviaal tot vervelend. Als je verder gaat, kun je command-line toepassingen combineren om een reeks werkzaamheden aan elkaar te rijgen om een gewenst resultaat te bereiken. Door bestaande command-line commando’s te leren, kunt u uw productiviteit verhogen en beter begrijpen welke mogelijkheden voorhanden zijn en welke taken u misschien zelf moet uitvoeren.
Wanneer u een command-line tool ontwerpt, moet u echt aandacht besteden aan wie, of wat, uw doelpubliek is. Bijvoorbeeld, als alle mensen die dit gereedschap gaan gebruiken hetzelfde besturingssysteem en dezelfde omgeving gebruiken, dan heb je de meeste flexibiliteit om te profiteren van het volledige ecosysteem. Als je echter je command-line app moet implementeren om te werken op meerdere besturingssystemen, dan heb je nu de tools die je kan gebruiken beperkt tot wat beschikbaar is op al die systemen — of je moet uitzonderingen voor elk besturingssysteem implementeren in je app. (Dat kan erg vervelend zijn om te onderzoeken en te testen.)
De meest gebruikte oplossing om met verschillende OS omgevingen om te gaan is om OS-specifieke externe tools waar mogelijk te vermijden of om de verantwoordelijkheid te delegeren aan bibliotheken die al het harde werk hebben gedaan om functies te implementeren die voor meerdere architecturen werken. Hoe meer je de implementatie kunt beperken tot één kerntaal zonder afhankelijk te zijn van externe afhankelijkheden, hoe makkelijker het zal zijn om je project te onderhouden.
De componenten van een commando-regel applicatie in Ruby
Er zijn drie aandachtsgebieden bij het bouwen van een commando-regel applicatie: de invoer, uitvoer, en alles daartussenin. Als je een eenmalige command-line applicatie ontwikkelt die simpelweg een invoer of bron aanneemt en deze verwerkt/opmaakt en de informatie uitspuugt, dan is je werk vrij eenvoudig. Als u echter een gebruikersinterface ontwikkelt met menu’s om doorheen te navigeren, wordt het allemaal wat ingewikkelder.
Invoer verwerken
Om te beginnen met het verwerken van invoer, hebben we parameters die aan uw toepassing kunnen worden meegegeven. Deze argumenten zijn de meest typische manier om een commando te starten, met details over hoe u wilt dat het wordt uitgevoerd en vermijden de noodzaak van menusystemen. In dit voorbeeld:
ruby -e "puts RUBY_VERSION"
Ruby is het command-line programma dat wordt uitgevoerd, -e
wordt een vlag genoemd, en "puts RUBY_VERSION"
is de waarde die voor de vlag wordt gegeven. In dit geval betekent Ruby’s vlag -e
om het volgende als Ruby code uit te voeren. Als ik het bovenstaande op de commandoregel uitvoer, krijg ik 2.4.0
terug, uitgeprint naar de standaarduitvoer (die gewoon op de volgende regel staat).
Argument input
Argument input, of parameters, zijn alle extra stukjes tekst die na een commando worden ingevoerd en gescheiden door een spatie. Bijna alle commando’s laten een help-flag argument toe. Een vlag-argument wordt voorafgegaan door een of twee streepjes.
De standaard help-vlaggen zijn -h
, en --help
. In de MSDOS dagen, was het (en is het misschien nog steeds) /?
. Ik weet niet of de trend voor vlaggen in Windows is doorgegaan met het gebruik van forward-slash als vlagmarkering, maar cross-platform command-line scripts gebruiken tegenwoordig streepjes.
In Ruby wordt de invoer van een commandoregel in twee verschillende variabelen geplaatst: ARGV en ARGF. ARGV behandelt de input parameters als een array van strings; ARGF is voor het behandelen van stromen van gegevens. U kunt ARGV direct gebruiken voor parameters, maar dit kan meer werk zijn dan u nodig heeft. Er zijn een paar Ruby bibliotheken gebouwd voor het werken met command-line argumenten.
OptionParser
OptionParser heeft het voordeel dat het wordt meegeleverd met Ruby, dus het is geen externe afhankelijkheid. OptionParser geeft u een gemakkelijke manier om zowel weer te geven welke command-line opties beschikbaar zijn als de invoer te verwerken in elk Ruby object dat u maar wilt. Hier is een uittreksel van een van mijn command-line tools 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!
In dit voorbeeld heeft elk opts.on
blok code om uit te voeren als de vlag werd doorgegeven op de commandoregel. De onderste vier opties (binnen hun blok) voegen eenvoudig de vlag info toe in een array voor mij om later te gebruiken.
De eerste heeft Array
gegeven als een van de parameters aan on
, dus de input voor deze vlag zal worden omgezet naar een Ruby array en opgeslagen in mijn hash genaamd options
onder de sleutel filters
.
De rest van de parameters gegeven aan de on
methode zijn de vlag details en de beschrijving. Zowel de korte als de lange vlaggen zullen werken om de code uit te voeren die in het volgende blok wordt gegeven.
OptionParser heeft standaard ook de -h
en --help
vlaggen ingebouwd, dus u hoeft het wiel niet opnieuw uit te vinden. Typ gewoon dfm -h
voor de bovenstaande command-line tool, en het geeft netjes een behulpzame beschrijving:
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 is een beetje langdradig als het aankomt op het uitschrijven van de command-line definities. De gem Slop is ontworpen om u in staat te stellen uw eigen command-line parsing te schrijven met veel minder inspanning. In plaats van dat u de codeblokken in de vlagdefinitie moet opgeven, maakt Slop een object aan dat u in uw toepassing kunt opvragen om te zien of een vlag werd gegeven en welke waarde(n) ervoor werden opgegeven.
# 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 }
Deze eenvoud kan helpen bij het vereenvoudigen van uw codebasis en uw testsuite, en ook uw ontwikkelingstijd versnellen.
Output (STDOUT)
Wanneer u een eenvoudig opdrachtregelprogramma schrijft, wilt u vaak dat het de resultaten van het uitgevoerde werk uitvoert. Het doel van het gereedschap bepaalt voor een groot deel hoe de uitvoer eruit moet zien.
U kunt de uitvoergegevens opmaken als een eenvoudige tekenreeks, lijst, hash, geneste array, JSON, XML of een andere vorm van gegevens die moet worden geconsumeerd. Als je gegevens over een netwerkverbinding worden gestreamd, dan wil je de gegevens verpakken in een strak verpakte string van gegevens. Als het bedoeld is om door de ogen van een gebruiker te worden gezien, dan wil je het op een presenteerbare manier uitbreiden.
Veel bestaande Linux/Mac command-line tools kunnen details in paren of reeksen van waarden afdrukken. Informatie kan worden gescheiden door een dubbele punt, spaties, tabs, en inspringingsblokken. Als je niet zeker weet hoe het precies gebruikt kan worden, ga dan voor de eenvoudigste en meest presenteerbare manier om de gegevens te presenteren.
Een voorbeeld van een doel dat je misschien moet overwegen is een API-testtool. Veel API’s leveren een JSON-respons en kunnen worden benaderd met een command-line tool zoals curl
. Als bandbreedte een zorg is, gebruik dan JSON’s to_json
methode, maar als het bedoeld is voor lokaal machine werk, gebruik dan 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"# }# }
Je kunt ook YAML gebruiken voor data.
require 'yaml'puts x.to_yaml# ---# hello: world# :this:# apple: 4# :tastes: delicious
Als je meer complexe uitvoer mooi geformatteerd wilt hebben, dan raad ik je ten zeerste aan om de awesome_print gem te gebruiken. Dit geeft u specifieke controle over uw presentatie op output.
Standaardfout (STDERR)
Dit is een ander soort output dat kan voorkomen bij command-line toepassingen. Wanneer er iets mis is of is misgegaan, is het gebruikelijk om het command-line gereedschap te laten schrijven naar de uitvoer die bekend staat als STDERR. Dit verschijnt als normale uitvoer, maar andere hulpprogramma’s kunnen controleren dat de opdracht niet succesvol was.
STDERR.puts "Oops! You broke it!"
Het is gebruikelijker om een Logger te gebruiken om specifieke details over fouten te schrijven. U kunt dat naar STDERR uitvoer op de opdrachtregel of eventueel een logbestand leiden.
Gebruikersinterface (STDIN en STDOUT)
Het schrijven van een gebruikersinterface kan een van de meest lonende dingen zijn om te doen. Het stelt je in staat om wat artistiek ontwerp toe te passen en de gebruiker verschillende manieren van interactie te bieden.
De minimale gebruikersinterface is een weergave van wat tekst met een prompt die wacht op invoer. Het kan er zo simpel uitzien als:
Who is your favorite super hero /Batman/Wonder-Woman ?
Het bovenstaande geeft de vraag aan, de opties, en een standaard optie voor het geval de gebruiker ervoor kiest om op de enter toets te drukken zonder iets in te voeren. In gewone Ruby zou dit er zo uitzien:
favorite = "Superman"printf "Who is your favorite hero /Batman/Wonder Woman?"input = gets.chompfavorite = input unless input.empty?
Dit is erg grove code om te gebruiken voor invoer en uitvoer, en als je een UI gaat schrijven, raad ik je sterk aan om een gem als highline of tty uit te proberen.
Met de edelsteen highline zou je het bovenstaande kunnen schrijven als:
require 'highline/import'favorite = ask("Who is your favorite hero Superman/Batman/Wonder Woman?") {|question| question.in = question.default = "Superman"}
Hier zal highline de vraag tonen, de standaard tonen, en eventuele onjuiste ingevoerde opties afvangen, door de gebruiker te melden dat hij niet een van de geboden opties heeft geselecteerd.
Zowel highline als tty hebben een enorme hoeveelheid extra dingen die je kunt doen voor een menu en gebruikerservaring, met aardigheidjes zoals het toevoegen van kleuren aan je scherm. Maar ook hier moet je rekening houden met wie je doelgroep is.
Hoe meer visuele ervaring je biedt, hoe meer je aandacht moet besteden aan de cross-platform mogelijkheden van deze tools. Niet alle opdrachtregels behandelen dezelfde presentatiegegevens op dezelfde manier, wat een slechte gebruikerservaring oplevert.
Cross-platform compatibiliteit
Het grote nieuws is dat Ruby veel van de oplossingen heeft die nodig zijn voor tooling over verschillende besturingssystemen. Wanneer Ruby wordt gecompileerd voor een specifiek besturingssysteem, wordt het bronbestand voor RbConfig gegenereerd met absolute waarden die eigen zijn aan het systeem waarop het is gebouwd, opgeslagen in hash-formaat. Het is daarom de sleutel tot het detecteren en gebruiken van OS functies.
Om dit bestand te zien met uw favoriete tekst editor, kunt u de volgende Ruby code uitvoeren:
editor = "sublime" # your preferred editor hereexec "#{editor} #{RbConfig.method(:ruby).source_location}"
Dit zal u alles laten zien op een presenteerbare manier, wat naar mijn gevoel beter is dan het zien van de hash via RbConfig::CONFIG
. Deze hash bevat nuttige dingen zoals de gebruikte commando’s voor het omgaan met het bestandssysteem, Ruby versie, systeem architectuur, waar alles wat belangrijk is voor Ruby zich bevindt, en andere dingen zoals dit.
Om het besturingssysteem te controleren, kunt u het volgende doen:
case RbConfig::CONFIGwhen /mingw32|mswin/ # Probably a Windows operating systemelse # Most likely a Unix compatible system like BSD/Mac/Linuxend
Voor andere besturingssysteem-specifieke waarden, kunt u kijken in de broncode voor Gem::Platform.
Nou, de bestandssysteem-specifieke commando’s die hier zijn opgeslagen, zijn niet bedoeld om te worden gebruikt vanuit deze bron. Ruby heeft daarvoor de klassen Dir, File, en Pathname geschreven.
Wanneer je moet weten of een uitvoerbaar bestand bestaat in het PATH van het systeem, dan zul je MakeMakefile.find_executable willen gebruiken. Ruby ondersteunt het bouwen van C extensies en een van de leuke features die ze daarvoor hebben toegevoegd is deze mogelijkheid om uit te zoeken of de executable bestaat om aan te roepen.
Maar dit zal een log bestand genereren in de huidige directory van je systeem, iedere keer dat je het uitvoert. Dus om te voorkomen dat dit logbestand wordt geschreven, moet u het volgende doen:
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
Wanneer u een visueel menu in een venster op een opdrachtregel tekent, verdient het de voorkeur dat het menu op een vaste positie blijft staan wanneer de weergave wordt bijgewerkt. De eenvoudigste manier om dit te doen is om de opdrachtregel te wissen tussen elke keer dat de volledige weergave wordt geschreven.
In het bovenstaande voorbeeld heb ik gezocht naar het commando clear
. Op Windows is het shell commando om de opdrachtregel te wissen cls
, maar je zult er geen uitvoerbaar programma voor kunnen vinden omdat het deel uitmaakt van command.com
’s interne code.
Het goede nieuws is dat het pakken van de string uitvoer van Linux’s clear
commando deze escape code sequentie oplevert: \e[3J\e[H\e[2J
. Ik en anderen hebben dit getest onder Windows, Mac, en Linux, en dit doet precies wat we willen: het scherm leegmaken om het opnieuw te tekenen.
Deze escape string heeft drie verschillende acties die hij uitvoert:
CLEAR = (ERASE_SCOLLBACK = "\e[3J") + (CURSOR_HOME = "\e[H") + (ERASE_DISPLAY = "\e[2J")
Hoewel, het verdient de voorkeur om een command-line bibliotheek te gebruiken in plaats van zelf escape codes te schrijven. Maar deze is het vermelden waard.
Test STDIN/STDOUT/STDERR
Bij alles wat naar iets als STDOUT of STDERR schrijft, is het het verstandigst om een interne variabele in uw UI Ruby-object te maken die kan worden gewijzigd met een optionele parameter in new
. Dus wanneer uw programma normaal draait, zal de waarde STDOUT
zijn. Maar wanneer u tests schrijft, geeft u StringIO.new
door, waartegen u gemakkelijk kunt testen.
Wanneer u IO probeert te lezen van een commando dat buiten Ruby wordt uitgevoerd, is het testen een beetje meer betrokken. U zult waarschijnlijk moeten kijken naar Open3.popen3 om elke IO stroom STDIN, STDOUT, en STDERR af te handelen.
Bash commando’s gebruiken
Bash is naar Windows (WSL) gekomen! Dit betekent dat u toegang heeft tot bash commando’s in de overgrote meerderheid van de besturingssystemen die wereldwijd gebruikt worden. Dit opent meer mogelijkheden voor uw eigen command-line tool.
Het is gebruikelijk om commando’s door elkaar te “pijpen”, waarbij de uitvoer van het ene commando als een stroom naar het volgende commando wordt gestuurd. Als u dit weet, kunt u overwegen ondersteuning voor streaming-input toe te voegen, of te bedenken hoe u uw uitvoer beter kunt formatteren voor gebruik door andere opdrachtregeltools.
Hier volgt een voorbeeld van een regex-substitutie met Bash’s sed
-opdracht waarvan de invoer wordt gepijpt door echo:
echo hello | sed "s/ll/ck n/g"
Hiermee wordt heck no
uitgevoerd. Hoe meer je leert over Bash, hoe beter je in staat zult zijn om dingen te doen op een commandoregel en hoe beter je in staat zult zijn om zelf betere commandoregeltools te schrijven.
Samenvatting
Er valt veel te leren als het gaat om het schrijven van je eigen commandoregeltools. Hoe meer situaties je moet onderkennen, hoe complexer de dingen worden.
Neem de tijd om de tools te leren die je tot je beschikking hebt, en je zult veel beloningen oogsten waarvan je niet wist dat je ze miste. Ik wens je het allerbeste.