Når det kommer til softwareudvikling, er størstedelen af de værktøjer, der er tilgængelige for os, kommandolinjeprogrammer. Det er også værd at bemærke, at mange af de værktøjer, der anvendes på kommandolinjen, er ret kraftfulde i forhold til, hvad de kan udrette, fra det trivielle til det kedelige. Hvis man går videre, kan man kombinere kommandolinjeprogrammer for at kæde en sekvens af arbejde sammen for at opnå et ønsket resultat. Ved at lære eksisterende kommandolinjekommandoer kan du øge din produktivitet og bedre forstå, hvilke muligheder der er til rådighed, og hvilke opgaver du måske skal gennemføre på egen hånd.
Når du designer et kommandolinjeværktøj, skal du virkelig være opmærksom på, hvem eller hvad din målgruppe er. Hvis f.eks. alle de personer, der skal bruge dette værktøj, bruger det samme operativsystem og miljø, har du den største fleksibilitet til at udnytte hele økosystemet. Hvis du derimod skal implementere din kommandolinjeapp til at fungere på tværs af flere operativsystemer, har du nu begrænset de værktøjer, du kan bruge, til det, der er tilgængeligt på alle disse systemer – eller du skal implementere undtagelser for hvert operativsystem i din app. (Det kan være meget besværligt at undersøge og teste for.)
Den løsning, der oftest anvendes til at håndtere forskellige OS-miljøer, er at undgå OS-specifikke eksterne værktøjer, når det er muligt, eller at uddelegere ansvaret til biblioteker, der allerede har gjort det hårde arbejde med at implementere funktioner, der fungerer for flere arkitekturer. Jo mere du kan holde implementeringen nede på ét kernesprog uden at være afhængig af eksterne afhængigheder, jo nemmere bliver det at vedligeholde dit projekt.
Komponenterne i en kommandolinjeapplikation i Ruby
Der er tre områder at tage fat på, når du opbygger en kommandolinjeapplikation: input, output og alt derimellem. Hvis du udvikler et enkeltstående kommandolinjeværktøj, der blot tager et input eller en kilde og behandler/formaterer det og spytter oplysningerne ud, er dit arbejde ret simpelt. Men hvis du udvikler en brugergrænseflade med menuer at navigere i, begynder tingene at blive mere komplicerede.
Behandling af input
For at begynde at behandle input har vi parametre, som kan gives til dit program. Disse argumenter er den mest typiske måde at starte en kommando på, med detaljer om, hvordan du ønsker, at den skal udføres, så du undgår behovet for menusystemer. I dette eksempel:
ruby -e "puts RUBY_VERSION"
Ruby er det kommandolinjeprogram, der skal køres, -e
kaldes et flag, og "puts RUBY_VERSION"
er den værdi, der er givet for flaget. I dette tilfælde betyder Rubys flag -e
, at følgende skal udføres som Ruby-kode. Når jeg kører ovenstående på kommandolinjen, får jeg tilbage 2.4.0
udskrevet til standard output (som blot vises på næste linje).
Argument input
Argument input, eller parametre, er alle de ekstra tekststykker, der indtastes efter en kommando og adskilles af et mellemrum. Næsten alle kommandoer giver mulighed for et hjælpeflag-argument. Et flag-argument har en streg eller to foran sig.
Standard hjælp-flagene er -h
, og --help
. Tilbage i MSDOS-dagene var det (og er måske stadig) /?
. Jeg ved ikke, om tendensen for flag i Windows er blevet ved med at bruge forward-slash som flagmarkør, men i disse dage bruger kommandolinjeskripter på tværs af platforme bindestreger.
I Ruby placeres input fra en kommandolinje i to forskellige variabler: ARGV og ARGF. ARGV håndterer inputparametrene som et array af strenge; ARGF er beregnet til håndtering af datastrømme. Du kan bruge ARGV til parametre direkte, men det kan være mere arbejde, end du har brug for. Der er et par Ruby-biblioteker, der er bygget til at arbejde med kommandolinjeargumenter.
OptionParser
OptionParser har den fordel, at den er inkluderet med Ruby, så den er ikke en ekstern afhængighed. OptionParser giver dig en nem måde både at vise, hvilke kommandolinjeindstillinger der er tilgængelige, og at behandle input til et hvilket som helst Ruby-objekt, du ønsker. Her er et uddrag fra et af mine kommandolinjeværktøjer 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 dette eksempel har hver opts.on
-blok kode, der skal udføres, hvis flaget blev overført på kommandolinjen. De nederste fire muligheder er (inden for deres blok) blot at tilføje flagoplysningerne til et array, så jeg kan bruge dem senere.
Den første har Array
givet som en af parametrene til on
, så input for dette flag vil blive konverteret til et Ruby-array og gemt i min hash ved navn options
under nøglen filters
.
Resten af de parametre, der er givet til on
-metoden, er flagoplysningerne og beskrivelsen. Både det korte og det lange flag vil fungere til at udføre den kode, der er givet i den følgende blok.
OptionParser har også flagene -h
og --help
indbygget som standard, så du behøver ikke at genopfinde hjulet igen. Du skal blot skrive dfm -h
for ovenstående kommandolinjeværktøj, og det udsender pænt en nyttig beskrivelse:
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 er en smule mundret, når det gælder om at skrive kommandolinje-definitionerne ud. Gemmen Slop er designet til at gøre det muligt for dig at skrive din egen kommandolinjeparsing med meget mindre besvær. I stedet for at du skal angive blokkene af kode i flagdefinitionen, opretter Slop simpelthen et objekt, som du kan forespørge i dit program for at se, om der blev givet et flag, og hvilken værdi(er) der eventuelt er angivet for det.
# 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 }
Denne enkelhed kan være med til at forenkle din kodebase og din testsuite samt fremskynde din udviklingstid.
Output (STDOUT)
Når du skriver et simpelt kommandolinjeværktøj, ønsker du ofte, at det skal udgive resultaterne af det udførte arbejde. Hvis du holder dig for øje, hvad målet for dette værktøj er, vil det i høj grad bestemme, hvordan du ønsker, at outputtet skal se ud.
Du kan formatere outputdataene som en simpel streng, liste, hash, nestet array, JSON, XML eller en anden form for data, der skal forbruges. Hvis dine data skal streames over en netværksforbindelse, skal du pakke dataene i en tætpakket streng af data. Hvis det er meningen, at en brugers øjne skal se det, så vil du gerne udvide det på en præsentabel måde.
Mange eksisterende Linux/Mac-kommandolinjeværktøjer kan udskrive detaljer i par eller sæt af værdier. Oplysninger kan adskilles med kolon, mellemrum, tabulatorer og indrykningsblokke. Når du ikke er sikker på, hvordan det præcist kan bruges, skal du gå efter den enkleste og mest præsentable måde at præsentere dataene på.
Et eksempel på et mål, som du måske skal overveje, er et API-testværktøj. Mange API’er giver et JSON-svar og kan tilgås med et kommandolinjeværktøj som curl
. Hvis båndbredde er et problem, så brug JSON’s to_json
metode, men hvis det er beregnet til arbejde på en lokal maskine, så brug 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 ligeledes bruge YAML til data.
require 'yaml'puts x.to_yaml# ---# hello: world# :this:# apple: 4# :tastes: delicious
Hvis du vil have mere komplekse output pænt formateret, så kan jeg varmt anbefale at bruge awesome_print gem’en. Dette vil give dig specifik kontrol over din præsentation på output.
Standardfejl (STDERR)
Dette er en anden form for output, som kan forekomme fra kommandolinjeprogrammer. Når noget er galt eller er gået galt, er det almindeligt, at kommandolinjeværktøjet skriver til det output, der er kendt som STDERR. Det vises som almindelig output, men andre værktøjer kan verificere, at kommandoen ikke lykkedes.
STDERR.puts "Oops! You broke it!"
Den mere almindelige praksis er at bruge en Logger til at skrive specifikke detaljer om fejl. Du kan dirigere det til STDERR-udgangen på kommandolinjen eller eventuelt til en logfil.
Brugergrænseflade (STDIN og STDOUT)
At skrive en brugergrænseflade kan være en af de mest givende ting at udføre. Det giver dig mulighed for at anvende noget kunstnerisk design og give forskellige måder for en bruger at interagere på.
Den minimale brugergrænseflade er en visning af noget tekst med en prompt, der venter på input. Det kan se så simpelt ud som:
Who is your favorite super hero /Batman/Wonder-Woman ?
Overstående angiver spørgsmålet, mulighederne og en standardindstilling, hvis brugeren vælger at trykke på enter-tasten uden at indtaste noget. I ren Ruby ville dette se ud som:
favorite = "Superman"printf "Who is your favorite hero /Batman/Wonder Woman?"input = gets.chompfavorite = input unless input.empty?
Det er meget grov kode at bruge til input og output, og hvis du skal skrive en UI, kan jeg varmt anbefale at prøve en gem som highline eller tty.
Med highline-perlen kunne du skrive ovenstående som:
require 'highline/import'favorite = ask("Who is your favorite hero Superman/Batman/Wonder Woman?") {|question| question.in = question.default = "Superman"}
Her vil highline vise spørgsmålet, vise standard og fange eventuelle forkerte indtastede valgmuligheder og give brugeren besked om, at de ikke har valgt en af de angivne muligheder.
Både highline og tty har en enorm mængde yderligere ting, du kan gøre for en menu og brugeroplevelse, med finesser som f.eks. at tilføje farver til dit display. Men igen skal du tage hensyn til, hvem din målgruppe er.
Desto mere af en visuel oplevelse du tilbyder, desto mere skal du være opmærksom på disse værktøjers muligheder på tværs af platforme. Ikke alle kommandolinjer håndterer de samme præsentationsdata på samme måde, hvilket skaber en dårlig brugeroplevelse.
Kompatibilitet på tværs af platforme
Den gode nyhed er, at Ruby har mange af de løsninger, der er nødvendige for værktøj på tværs af forskellige operativsystemer. Når Ruby kompileres til et bestemt styresystem, genereres kildefilen til RbConfig med absolutte værdier, der er native for det system, det blev bygget på, gemt i hash-format. Det er derfor nøglen til at opdage og bruge OS-funktioner.
For at se denne fil med din foretrukne teksteditor kan du køre følgende Ruby-kode:
editor = "sublime" # your preferred editor hereexec "#{editor} #{RbConfig.method(:ruby).source_location}"
Det vil vise dig alt på en præsentabel måde, hvilket jeg synes er bedre end at se hash’en via RbConfig::CONFIG
. Denne hash indeholder nyttige ting som de kommandoer, der bruges til at håndtere filsystemet, Ruby-versionen, systemarkitekturen, hvor alt vigtigt for Ruby ligger på, og andre ting som dette.
For at kontrollere styresystemet kan du gøre:
case RbConfig::CONFIGwhen /mingw32|mswin/ # Probably a Windows operating systemelse # Most likely a Unix compatible system like BSD/Mac/Linuxend
For andre styresystemspecifikke værdier kan du kigge i kildekoden til Gem::Platform.
Nu er det ikke meningen, at de filsystemspecifikke kommandoer, der er gemt her, skal bruges fra denne ressource. Ruby har klasserne Dir, File og Pathname skrevet til det.
Når du har brug for at vide, om en eksekverbar fil findes i systemets PATH, skal du bruge MakeMakefile.find_executable. Ruby understøtter opbygning af C-udvidelser, og en af de gode funktioner, de har tilføjet til det, er denne mulighed for at finde ud af, om den eksekverbare fil findes til at kalde.
Men dette vil generere en logfil i dit systems aktuelle mappe, hver gang du kører det. Så for at undgå at skrive denne logfil skal du gøre følgende:
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 tegner en visuel vinduesmenu på en kommandolinje, er det at foretrække, at menuen skal forblive i en fast position, når skærmen opdateres. Den enkleste måde at gøre dette på er at rydde kommandolinjen mellem hver skrivning af den fulde visning.
I eksemplet ovenfor søgte jeg efter kommandoen clear
. På Windows er shell-kommandoen til at rydde kommandolinjen cls
, men du vil ikke kunne finde en eksekverbar fil til den, fordi den er en del af command.com
s interne kode.
Den gode nyhed er, at det giver denne escape-kode-sekvens, hvis du tager fat i strengudgangen fra Linux’ clear
-kommando: \e[3J\e[H\e[2J
. Jeg og andre har testet dette på tværs af Windows, Mac og Linux, og det gør præcis det, vi ønsker: at rydde skærmen, så den kan tegnes på ny.
Denne escape-streng har tre forskellige handlinger, den udfører:
CLEAR = (ERASE_SCOLLBACK = "\e[3J") + (CURSOR_HOME = "\e[H") + (ERASE_DISPLAY = "\e[2J")
Det er dog at foretrække at bruge et kommandolinjebibliotek frem for selv at skrive escape-koder. Men denne er værd at nævne.
Test af STDIN/STDOUT/STDERR
Med alt, der skriver til noget som STDOUT eller STDERR, vil det være klogest at oprette en intern variabel i dit Ruby-objekt i brugergrænsefladen, som kan ændres med en valgfri parameter til new
. Så når dit program normalt kører, vil værdien være STDOUT
. Men når du skriver tests, vil du sende StringIO.new
, som du nemt kan teste imod.
Når du forsøger at læse IO fra en kommando, der udføres uden for Ruby, er testen lidt mere indviklet. Du skal sandsynligvis kigge på Open3.popen3 for at håndtere hver enkelt IO-stream STDIN, STDOUT og STDERR.
Brug af Bash-kommandoer
Bash er kommet til Windows (WSL)! Det betyder, at du har adgang til bash-kommandoer på tværs af langt de fleste operativsystemer, der anvendes verden over. Det åbner flere muligheder for dit eget kommandolinjeværktøj.
Det er typisk at “pipe”-kommandoer gennem hinanden, så output fra en kommando sendes som en strøm til den næste. Når du ved dette, kan du overveje at tilføje understøttelse af streaming input eller overveje, hvordan du bedre kan formatere dit output til forbrug af andre kommandolinjeværktøjer.
Her er et eksempel på en regex-substitution med Bashs sed
-kommando, der får sit input rørlagt fra echo:
echo hello | sed "s/ll/ck n/g"
Dette giver output heck no
. Jo mere du lærer om Bash, jo bedre er du i stand til at få ting udrettet på en kommandolinje, og jo bedre er du rustet til at skrive dine egne bedre kommandolinjeværktøjer.
Resumé
Der er meget at lære, når det gælder om at skrive dine egne kommandolinjeværktøjer. Jo flere situationer du skal tage højde for, jo mere komplekse bliver tingene.
Tag dig tid til at lære de værktøjer, du har ved hånden, og du vil høste mange belønninger, som du ikke vidste, at du gik glip af. Jeg ønsker dig alt held og lykke!