När det gäller programvaruutveckling är majoriteten av de verktyg som finns tillgängliga för oss kommandoradsprogram. Det är också väl värt att notera att många av de verktyg som används på kommandoraden är ganska kraftfulla i vad de kan åstadkomma, från det triviala till det tråkiga. Om man går vidare kan man kombinera kommandoradstillämpningar för att kedja ihop en sekvens av arbete för att uppnå ett önskat resultat. Om du lär dig befintliga kommandoradsfunktioner kan du öka din produktivitet och bättre förstå vilka möjligheter som finns till hands och vilka uppgifter du kan behöva genomföra på egen hand.
När du utformar ett kommandoradsverktyg måste du verkligen vara uppmärksam på vem, eller vad, som är din målgrupp. Om till exempel alla som kommer att använda verktyget använder samma operativsystem och miljö har du störst flexibilitet när det gäller att dra nytta av hela ekosystemet. Om du däremot behöver implementera din kommandoradsapp för att fungera i flera olika operativsystem har du nu begränsat de verktyg som du kan använda till vad som finns tillgängligt på alla dessa system – eller så måste du implementera undantag för varje operativsystem i din app. (Det kan vara mycket tråkigt att undersöka och testa för.)
Den lösning som oftast används för att hantera olika OS-miljöer är att undvika OS-specifika externa verktyg när det är möjligt eller att delegera ansvaret till bibliotek som redan har gjort det hårda arbetet med att implementera funktioner som fungerar för flera arkitekturer. Ju mer du kan hålla implementeringen till ett kärnspråk utan att vara beroende av externa beroenden, desto lättare blir det att underhålla ditt projekt.
Komponenterna i en kommandoradstillämpning i Ruby
Det finns tre områden att ta itu med när man bygger en kommandoradstillämpning: inmatning, utmatning och allt däremellan. Om du utvecklar ett enstaka kommandoradsverktyg som helt enkelt tar emot en inmatning eller källa och bearbetar/formaterar den och spottar ut informationen är ditt arbete ganska enkelt. Men om du utvecklar ett användargränssnitt med menyer att navigera i börjar det bli mer komplicerat.
Bearbetning av indata
För att börja bearbeta indata har vi parametrar som kan ges till ditt program. Dessa argument är det mest typiska sättet att starta ett kommando, med detaljer om hur du vill att det ska exekveras och undvika behovet av menysystem. I det här exemplet:
ruby -e "puts RUBY_VERSION"
Ruby är det kommandoradsprogram som körs, -e
kallas flagga och "puts RUBY_VERSION"
är det värde som ges för flaggan. I det här fallet innebär Rubys flagga -e
att följande ska exekveras som Ruby-kod. När jag kör ovanstående på kommandoraden får jag tillbaka 2.4.0
som skrivs ut till standardutgången (som helt enkelt visas på nästa rad).
Argumentinmatning
Argumentinmatning, eller parametrar, är alla de extra textbitar som skrivs in efter ett kommando och som är åtskilda med ett mellanslag. Nästan alla kommandon tillåter ett argument med hjälpflagga. Ett flaggargument har ett bindestreck eller två framför sig.
Standardhjälpflaggorna är -h
, och --help
. På MSDOS-tiden var det (och kan fortfarande vara) /?
. Jag vet inte om trenden för flaggor i Windows har fortsatt att använda forward-slash som flaggmarkör, men plattformsoberoende kommandoradsskript använder numera streck.
I Ruby placeras inmatningen från en kommandorad i två olika variabler: ARGV och ARGF. ARGV hanterar inmatningsparametrarna som en array av strängar; ARGF är till för att hantera dataströmmar. Du kan använda ARGV för parametrar direkt, men det kan vara mer arbete än vad du behöver göra. Det finns ett par Ruby-bibliotek som är byggda för att arbeta med kommandoradsargument.
OptionParser
OptionParser har fördelen att den ingår i Ruby, så den är inte ett externt beroende. OptionParser ger dig ett enkelt sätt att både visa vilka kommandoradsalternativ som finns tillgängliga och bearbeta inmatningen till vilket Ruby-objekt du vill. Här är ett utdrag från ett av mina kommandoradsverktyg 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!
I det här exemplet har varje opts.on
-block kod som ska exekveras om flaggan har lämnats in på kommandoraden. De fyra nedersta alternativen är (inom sitt block) helt enkelt att lägga till flagginformationen i en array så att jag kan använda den senare.
I det första alternativet har Array
angetts som en av parametrarna till on
, så inmatningen för den här flaggan kommer att konverteras till en Ruby-array och lagras i min hash med namnet options
under nyckeln filters
.
Resten av de parametrar som ges till on
-metoden är flagginformationen och beskrivningen. Både den korta och den långa flaggan fungerar för att exekvera koden som ges i följande block.
OptionParser har också flaggorna -h
och --help
inbyggda som standard, så du behöver inte uppfinna hjulet på nytt. Skriv bara dfm -h
för kommandoradsverktyget ovan, så får du en hjälpsam beskrivning:
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 är lite välformulerad när det gäller att skriva ut kommandoradsdefinitioner. Gemmen Slop är utformad för att du ska kunna skriva din egen kommandoradsanalys med mycket mindre ansträngning. Istället för att du måste tillhandahålla kodblocken i flaggdefinitionen skapar Slop helt enkelt ett objekt som du kan fråga i ditt program för att se om en flagga gavs och vilket eller vilka värden som kan ha angetts för den.
# 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 }
Denna enkelhet kan bidra till att förenkla din kodbas och din testföljd, samt påskynda din utvecklingstid.
Output (STDOUT)
När man skriver ett enkelt kommandoradsverktyg vill man ofta att det ska ge ut resultatet av det utförda arbetet. Att ha i åtanke vad målet för verktyget är avgör till stor del hur du vill att utdata ska se ut.
Du kan formatera utdata som en enkel sträng, lista, hash, nested array, JSON, XML eller annan form av data som ska konsumeras. Om dina data ska strömmas över en nätverksanslutning vill du packa datan till en tätt packad sträng av data. Om det är tänkt att en användares ögon ska se det, vill du expandera det på ett presentabelt sätt.
Många befintliga Linux/Mac-kommandoradsverktyg kan skriva ut detaljer i par eller uppsättningar av värden. Information kan separeras med kolon, mellanslag, tabulatorer och indragningsblock. När du inte är säker på exakt hur det kan användas, välj det enklaste och mest presentabla sättet att presentera uppgifterna.
Ett exempel på ett mål som du kan behöva överväga är ett verktyg för API-testning. Många API:er ger ett JSON-svar och kan nås med ett kommandoradsverktyg som curl
. Om bandbredd är ett problem kan du använda JSON:s to_json
-metod, men om det är tänkt för arbete på en lokal maskin kan du använda 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"# }# }
Du kan på samma sätt använda YAML för data.
require 'yaml'puts x.to_yaml# ---# hello: world# :this:# apple: 4# :tastes: delicious
Om du vill ha mer komplexa utdata som är snyggt formaterade kan jag starkt rekommendera att du använder awesome_print gem. Detta ger dig specifik kontroll över din presentation av utdata.
Standardfel (STDERR)
Det här är en annan typ av utdata som kan förekomma från kommandoradstillämpningar. När något är fel eller har gått fel är det vanligt att kommandoradsverktyget skriver till den utdata som kallas STDERR. Det kommer att visas som vanlig utdata, men andra verktyg kan verifiera att kommandot inte lyckades.
STDERR.puts "Oops! You broke it!"
Den vanligare metoden är att använda en Logger för att skriva specifika detaljer om fel. Du kan dirigera det till STDERR-utgången på kommandoraden eller möjligen till en loggfil.
Användargränssnitt (STDIN och STDOUT)
Att skriva ett användargränssnitt kan vara en av de mest givande sakerna att åstadkomma. Det ger dig möjlighet att tillämpa lite konstnärlig design och tillhandahålla olika sätt för en användare att interagera.
Det minimala användargränssnittet är en visning av viss text med en uppmaning som väntar på inmatning. Det kan se så enkelt ut som:
Who is your favorite super hero /Batman/Wonder-Woman ?
Ovanstående anger frågan, alternativen och ett standardalternativ om användaren väljer att trycka på enter-tangenten utan att skriva in något. I vanlig Ruby skulle detta se ut som:
favorite = "Superman"printf "Who is your favorite hero /Batman/Wonder Woman?"input = gets.chompfavorite = input unless input.empty?
Detta är mycket grov kod att använda för in- och utdata, och om du ska skriva ett användargränssnitt rekommenderar jag starkt att du prövar en gem som highline eller tty.
Med highline gem kan du skriva ovanstående som:
require 'highline/import'favorite = ask("Who is your favorite hero Superman/Batman/Wonder Woman?") {|question| question.in = question.default = "Superman"}
Här kommer highline att visa frågan, visa standardalternativet och fånga upp eventuella felaktiga alternativ som skrivits in, och meddela användaren att de inte har valt något av alternativen som tillhandahålls.
Både highline och tty har en stor mängd ytterligare saker som du kan göra för en meny och en användarupplevelse, med finesser som att lägga till färger på din skärm. Men återigen måste du ta hänsyn till vem din målgrupp är.
Desto mer av en visuell upplevelse du tillhandahåller, desto mer måste du vara uppmärksam på dessa verktygs plattformsoberoende möjligheter. Alla kommandorader hanterar inte samma presentationsdata på samma sätt, vilket skapar en dålig användarupplevelse.
Tvärplattformskompatibilitet
De goda nyheterna är att Ruby har en hel del av de lösningar som behövs för att skapa verktyg i olika operativsystem. När Ruby kompileras för ett visst operativsystem genereras källfilen för RbConfig med absoluta värden som är inhemska för det system som det byggdes på, sparade i hash-format. Det är därför nyckeln till att upptäcka och använda OS-funktioner.
För att se den här filen med din favorittextredigerare kan du köra följande Ruby-kod:
editor = "sublime" # your preferred editor hereexec "#{editor} #{RbConfig.method(:ruby).source_location}"
Detta kommer att visa dig allt på ett presentabelt sätt, vilket jag tycker är bättre än att se hash via RbConfig::CONFIG
. Denna hash innehåller användbara saker som de kommandon som används för att hantera filsystemet, Ruby-versionen, systemarkitekturen, var allt som är viktigt för Ruby finns och andra sådana saker.
För att kontrollera operativsystemet kan du göra:
case RbConfig::CONFIGwhen /mingw32|mswin/ # Probably a Windows operating systemelse # Most likely a Unix compatible system like BSD/Mac/Linuxend
För andra operativsystemspecifika värden kan du titta på källkoden för Gem::Platform.
Nu är de filsystemspecifika kommandon som lagras här inte avsedda att användas från denna resurs. Ruby har klasserna Dir, File och Pathname skrivna för detta.
När du behöver veta om en körbar fil finns i systemets PATH, vill du använda MakeMakefile.find_executable. Ruby har stöd för att bygga C-tillägg och en av de trevliga funktionerna de lagt till för detta är denna möjlighet att ta reda på om den körbara filen finns att anropa.
Men detta kommer att generera en loggfil i systemets aktuella katalog varje gång du kör den. Så för att undvika att skriva den här loggfilen måste du göra följande:
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
När du ritar en visuell fönstermeny på en kommandorad är det att föredra att menyn stannar i en fast position när skärmen uppdateras. Det enklaste sättet att göra detta är att rensa kommandoraden mellan varje skrivning av den fullständiga visningen.
I exemplet ovan sökte jag efter kommandot clear
. På Windows är skalkommandot för att rensa kommandoraden cls
, men du kommer inte att kunna hitta en körbar fil för det eftersom det är en del av command.com
s interna kod.
Den goda nyheten är att om du tar tag i strängutmatningen från Linux clear
-kommandot får du fram den här sekvensen av escape-koder: \e[3J\e[H\e[2J
. Jag och andra har testat detta i Windows, Mac och Linux, och detta gör exakt vad vi vill: rensar skärmen för att kunna rita om den.
Denna escape-sträng har tre olika åtgärder som den utför:
CLEAR = (ERASE_SCOLLBACK = "\e[3J") + (CURSOR_HOME = "\e[H") + (ERASE_DISPLAY = "\e[2J")
Hur som helst är det dock att föredra att använda ett kommandoradsbibliotek i stället för att skriva escape-koder själv. Men den här är väl värd att nämna.
Testning av STDIN/STDOUT/STDERR
Med allt som skriver till något som STDOUT eller STDERR är det klokast att skapa en intern variabel i ditt UI Ruby-objekt som kan ändras med en valfri parameter till new
. Så när ditt program normalt körs kommer värdet att vara STDOUT
. Men när du skriver tester kommer du att skicka in StringIO.new
, som du enkelt kan testa mot.
När du försöker läsa IO från ett kommando som körs utanför Ruby är testningen lite mer komplicerad. Du kommer förmodligen att behöva titta på Open3.popen3 för att hantera varje IO-ström STDIN, STDOUT och STDERR.
Användning av Bash-kommandon
Bash har kommit till Windows (WSL)! Detta innebär att du har tillgång till bash-kommandon i den stora majoriteten av de operativsystem som används världen över. Detta öppnar fler möjligheter för ditt eget kommandoradsverktyg.
Det är typiskt att ”pipa” kommandon genom varandra och skicka utdata från ett kommando som en ström till nästa. Med vetskap om detta kanske du vill överväga att lägga till stöd för strömmande indata, eller fundera på hur du bättre kan formatera ditt utdata för konsumtion av andra kommandoradsverktyg.
Här är ett exempel på en regex-substitution med Bashs sed
-kommando som får sin indata pipad från echo:
echo hello | sed "s/ll/ck n/g"
Detta ger ut heck no
. Ju mer du lär dig om Bash, desto bättre kan du få saker gjorda på en kommandorad och desto bättre rustad blir du för att skriva bättre egna kommandoradsverktyg.
Sammanfattning
Det finns mycket att lära sig när det gäller att skriva egna kommandoradsverktyg. Ju fler situationer du måste ta hänsyn till, desto mer komplexa blir saker och ting.
Ta dig tid att lära dig verktygen du har till hands, så kommer du att skörda många belöningar som du inte visste att du gick miste om. Jag önskar dig all lycka!