Wenn es um Softwareentwicklung geht, sind die meisten der uns zur Verfügung stehenden Tools Befehlszeilenanwendungen. Es ist auch erwähnenswert, dass viele der Werkzeuge, die auf der Kommandozeile verwendet werden, ziemlich mächtig sind, was die Möglichkeiten angeht, die sie bieten, von trivial bis langwierig. Darüber hinaus können Sie Befehlszeilenanwendungen kombinieren, um eine Reihe von Arbeitsschritten aneinander zu reihen, um ein gewünschtes Ergebnis zu erzielen. Durch das Erlernen bestehender Befehlszeilenbefehle können Sie Ihre Produktivität steigern und besser verstehen, welche Möglichkeiten Ihnen zur Verfügung stehen und welche Aufgaben Sie möglicherweise selbst durchführen müssen.
Wenn Sie ein Befehlszeilenwerkzeug entwerfen, müssen Sie wirklich darauf achten, wer oder was Ihr Zielpublikum ist. Wenn zum Beispiel alle Personen, die dieses Tool benutzen werden, das gleiche Betriebssystem und die gleiche Umgebung verwenden, dann haben Sie die größte Flexibilität, um das gesamte Ökosystem zu nutzen. Wenn Sie jedoch Ihre Befehlszeilenanwendung so implementieren müssen, dass sie auf mehreren Betriebssystemen funktioniert, haben Sie jetzt die Tools, die Sie verwenden können, auf die beschränkt, die auf all diesen Systemen verfügbar sind – oder Sie müssen Ausnahmen für jedes Betriebssystem in Ihre Anwendung implementieren. (Das kann sehr mühsam sein.)
Die am häufigsten verwendete Lösung für den Umgang mit verschiedenen Betriebssystemumgebungen besteht darin, möglichst keine betriebssystemspezifischen externen Tools zu verwenden oder die Verantwortung an Bibliotheken zu delegieren, die bereits die harte Arbeit der Implementierung von Funktionen für mehrere Architekturen geleistet haben. Je mehr Sie die Implementierung auf eine Kernsprache beschränken können, ohne von externen Abhängigkeiten abhängig zu sein, desto einfacher wird es sein, Ihr Projekt zu pflegen.
Die Komponenten einer Kommandozeilenanwendung in Ruby
Es gibt drei Bereiche, die bei der Entwicklung einer Kommandozeilenanwendung zu beachten sind: die Eingabe, die Ausgabe und alles dazwischen. Wenn Sie ein einmaliges Befehlszeilentool entwickeln, das einfach eine Eingabe oder Quelle entgegennimmt, sie verarbeitet/formatiert und die Informationen ausgibt, dann ist Ihre Arbeit recht einfach. Wenn Sie jedoch eine Benutzeroberfläche mit Menüs zum Navigieren entwickeln, werden die Dinge komplizierter.
Verarbeitung von Eingaben
Um mit der Verarbeitung von Eingaben zu beginnen, haben wir Parameter, die Ihrer Anwendung gegeben werden können. Diese Argumente sind die typischste Art und Weise, einen Befehl zu starten, mit Details, wie er ausgeführt werden soll, und vermeiden die Notwendigkeit von Menüsystemen. In diesem Beispiel:
ruby -e "puts RUBY_VERSION"
Ruby ist das auszuführende Befehlszeilenprogramm, -e
wird als Flag bezeichnet, und "puts RUBY_VERSION"
ist der für das Flag angegebene Wert. In diesem Fall bedeutet das Ruby-Flag -e
, dass das Folgende als Ruby-Code ausgeführt werden soll. Wenn ich den obigen Befehl auf der Kommandozeile ausführe, erhalte ich 2.4.0
auf der Standardausgabe zurück (die einfach in der nächsten Zeile erscheint).
Argumenteneingabe
Argumenteneingabe oder Parameter sind alle zusätzlichen Bits von Text, die nach einem Befehl eingegeben und durch ein Leerzeichen getrennt werden. Fast alle Befehle lassen ein Hilfsargument zu. Einem Flag-Argument werden ein oder zwei Bindestriche vorangestellt.
Die Standard-Hilfeflags sind -h
und --help
. In den MSDOS-Tagen war es (und ist es vielleicht immer noch) /?
. Ich weiß nicht, ob der Trend für Flags in Windows weiterhin den Schrägstrich als Flaggenmarkierung verwendet, aber plattformübergreifende Befehlszeilenskripte verwenden heutzutage Bindestriche.
In Ruby werden die Eingaben einer Befehlszeile in zwei verschiedenen Variablen abgelegt: ARGV und ARGF. ARGV behandelt die Eingabeparameter als Array von Strings; ARGF ist für die Behandlung von Datenströmen gedacht. Sie können ARGV direkt für Parameter verwenden, aber das kann mehr Arbeit sein, als Sie brauchen. Es gibt einige Ruby-Bibliotheken, die für die Arbeit mit Kommandozeilenargumenten entwickelt wurden.
OptionParser
OptionParser hat den Vorteil, dass er in Ruby enthalten ist, so dass er keine externe Abhängigkeit darstellt. Mit OptionParser können Sie auf einfache Weise sowohl die verfügbaren Kommandozeilenoptionen anzeigen als auch die Eingabe in ein beliebiges Ruby-Objekt verarbeiten. Hier ein Auszug aus einem meiner Kommandozeilentools 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 diesem Beispiel enthält jeder opts.on
-Block Code, der ausgeführt werden soll, wenn das Flag in der Kommandozeile übergeben wurde. Die unteren vier Optionen fügen (innerhalb ihres Blocks) einfach die Flaggeninformationen in ein Array ein, das ich später verwenden kann.
Bei der ersten wird Array
als einer der Parameter an on
übergeben, so dass die Eingabe für diese Flagge in ein Ruby-Array konvertiert und in meinem Hash namens options
unter dem Schlüssel filters
gespeichert wird.
Die restlichen Parameter, die an die Methode on
übergeben werden, sind die Flaggendetails und die Beschreibung. Sowohl die kurzen als auch die langen Flags funktionieren, um den im folgenden Block angegebenen Code auszuführen.
OptionParser hat auch die Flags -h
und --help
standardmäßig eingebaut, so dass Sie das Rad nicht neu erfinden müssen. Geben Sie einfach dfm -h
für das obige Kommandozeilentool ein, und es gibt eine hilfreiche Beschreibung aus:
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 ist ein bisschen langatmig, wenn es darum geht, die Kommandozeilendefinitionen zu schreiben. Das Gem Slop wurde entwickelt, um Ihnen zu ermöglichen, Ihr eigenes Kommandozeilen-Parsing mit viel weniger Aufwand zu schreiben. Anstatt die Codeblöcke in der Flaggendefinition bereitzustellen, erstellt Slop einfach ein Objekt, das Sie in Ihrer Anwendung abfragen können, um zu sehen, ob ein Flag angegeben wurde und welche(r) Wert(e) dafür bereitgestellt wurden.
# 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 }
Diese Einfachheit kann dazu beitragen, Ihre Codebasis und Ihre Testsuite zu vereinfachen und Ihre Entwicklungszeit zu verkürzen.
Ausgabe (STDOUT)
Wenn Sie ein einfaches Befehlszeilen-Tool schreiben, möchten Sie oft, dass es die Ergebnisse der durchgeführten Arbeit ausgibt. Wie die Ausgabe aussehen soll, hängt weitgehend davon ab, was das Ziel dieses Tools ist.
Die Ausgabedaten können als einfache Zeichenkette, Liste, Hash, verschachteltes Array, JSON, XML oder in einer anderen Form von zu verarbeitenden Daten formatiert werden. Wenn Ihre Daten über eine Netzwerkverbindung gestreamt werden sollen, müssen Sie die Daten in eine eng gepackte Zeichenkette packen. Wenn die Daten für die Augen des Benutzers bestimmt sind, sollten Sie sie in einer ansehnlichen Form ausgeben.
Viele bestehende Linux/Mac-Befehlszeilen-Tools können Details in Paaren oder Gruppen von Werten ausgeben. Die Informationen können durch einen Doppelpunkt, Leerzeichen, Tabulatoren und Einrückungsblöcke getrennt werden. Wenn Sie sich nicht sicher sind, wie genau die Daten verwendet werden sollen, wählen Sie die einfachste und vorzeigbarste Art der Darstellung.
Ein Beispiel für ein Ziel, das Sie in Betracht ziehen sollten, ist ein API-Testwerkzeug. Viele APIs liefern eine JSON-Antwort und können mit einem Befehlszeilentool wie curl
aufgerufen werden. Wenn die Bandbreite ein Problem ist, dann verwenden Sie die JSON-Methode to_json
, aber wenn es für die Arbeit auf einer lokalen Maschine gedacht ist, verwenden Sie 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"# }# }
Sie können auch YAML für Daten verwenden.
require 'yaml'puts x.to_yaml# ---# hello: world# :this:# apple: 4# :tastes: delicious
Wenn Sie eine komplexere Ausgabe haben wollen, die schön formatiert ist, dann empfehle ich Ihnen das awesome_print Gem. Damit haben Sie eine genaue Kontrolle über die Ausgabe.
Standard Error (STDERR)
Dies ist eine weitere Art von Ausgabe, die bei Kommandozeilenanwendungen auftreten kann. Wenn etwas nicht stimmt oder schief gegangen ist, ist es üblich, dass das Kommandozeilentool in die Ausgabe STDERR schreibt. Die Ausgabe erscheint als normale Ausgabe, aber andere Tools können überprüfen, dass der Befehl nicht erfolgreich war.
STDERR.puts "Oops! You broke it!"
Die üblichere Praxis ist die Verwendung eines Loggers, um spezifische Details über Fehler zu schreiben. Sie können dies in die STDERR-Ausgabe auf der Befehlszeile oder möglicherweise in eine Protokolldatei leiten.
Benutzeroberfläche (STDIN und STDOUT)
Das Schreiben einer Benutzeroberfläche kann eine der lohnendsten Aufgaben sein, die man erledigen kann. Sie erlaubt es, etwas künstlerisches Design anzuwenden und dem Benutzer verschiedene Möglichkeiten der Interaktion zu bieten.
Die minimale Benutzerschnittstelle ist eine Anzeige von Text mit einer Eingabeaufforderung, die auf eine Eingabe wartet. Sie kann so einfach aussehen wie:
Who is your favorite super hero /Batman/Wonder-Woman ?
Das obige Beispiel zeigt die Frage, die Optionen und eine Standardoption, falls der Benutzer die Eingabetaste drückt, ohne etwas einzugeben. In einfachem Ruby würde dies wie folgt aussehen:
favorite = "Superman"printf "Who is your favorite hero /Batman/Wonder Woman?"input = gets.chompfavorite = input unless input.empty?
Dies ist ein sehr grober Code für die Ein- und Ausgabe, und wenn Sie eine Benutzeroberfläche schreiben wollen, empfehle ich Ihnen dringend, einen Edelstein wie highline oder tty auszuprobieren.
Mit dem Highline-Gem könnte man das oben beschriebene wie folgt schreiben:
require 'highline/import'favorite = ask("Who is your favorite hero Superman/Batman/Wonder Woman?") {|question| question.in = question.default = "Superman"}
Hier zeigt Highline die Frage an, zeigt die Standardeinstellung an und fängt alle falsch eingegebenen Optionen ab, indem es den Benutzer benachrichtigt, dass er keine der angebotenen Optionen ausgewählt hat.
Beide, Highline und Tty, haben eine große Menge an zusätzlichen Dingen, die man für ein Menü und die Benutzererfahrung tun kann, mit Feinheiten wie dem Hinzufügen von Farben zur Anzeige. Aber auch hier müssen Sie berücksichtigen, wer Ihr Zielpublikum ist.
Je mehr visuelle Erfahrung Sie bieten, desto mehr müssen Sie auf die plattformübergreifenden Fähigkeiten dieser Werkzeuge achten. Nicht alle Befehlszeilen verarbeiten dieselben Präsentationsdaten auf dieselbe Weise, was zu einer schlechten Benutzererfahrung führt.
Plattformübergreifende Kompatibilität
Die gute Nachricht ist, dass Ruby viele der Lösungen bietet, die für das Tooling auf verschiedenen Betriebssystemen benötigt werden. Wenn Ruby für ein bestimmtes Betriebssystem kompiliert wird, wird die Quelldatei für RbConfig mit absoluten Werten generiert, die für das System, auf dem es gebaut wurde, typisch sind und im Hash-Format gespeichert werden. Sie ist daher der Schlüssel zum Erkennen und Verwenden von Betriebssystemfunktionen.
Um diese Datei mit Ihrem bevorzugten Texteditor zu sehen, können Sie den folgenden Ruby-Code ausführen:
editor = "sublime" # your preferred editor hereexec "#{editor} #{RbConfig.method(:ruby).source_location}"
Dies zeigt Ihnen alles in einer vorzeigbaren Weise, was ich für besser halte als den Hash über RbConfig::CONFIG
zu sehen. Dieser Hash enthält nützliche Dinge wie die Befehle, die für den Umgang mit dem Dateisystem verwendet werden, die Ruby-Version, die Systemarchitektur, wo sich alles, was für Ruby wichtig ist, befindet, und andere Dinge wie diese.
Um das Betriebssystem zu überprüfen, kannst du folgendes tun:
case RbConfig::CONFIGwhen /mingw32|mswin/ # Probably a Windows operating systemelse # Most likely a Unix compatible system like BSD/Mac/Linuxend
Für andere betriebssystemspezifische Werte kannst du dir den Quellcode von Gem::Platform ansehen.
Die hier gespeicherten dateisystemspezifischen Befehle sind nicht dafür gedacht, von dieser Ressource aus verwendet zu werden. Ruby hat dafür die Klassen Dir, File und Pathname geschrieben.
Wenn Sie wissen müssen, ob eine ausführbare Datei im PATH des Systems existiert, dann werden Sie MakeMakefile.find_executable verwenden wollen. Ruby unterstützt die Erstellung von C-Erweiterungen, und eine der netten Funktionen, die sie dafür hinzugefügt haben, ist die Möglichkeit, herauszufinden, ob die aufzurufende ausführbare Datei existiert.
Aber das erzeugt jedes Mal eine Protokolldatei im aktuellen Verzeichnis Ihres Systems, wenn Sie es ausführen. Um das Schreiben dieser Protokolldatei zu vermeiden, müssen Sie also Folgendes tun:
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
Wenn Sie ein visuelles Fenstermenü auf einer Befehlszeile zeichnen, ist es wünschenswert, dass das Menü in einer festen Position bleibt, wenn die Anzeige aktualisiert wird. Der einfachste Weg, dies zu erreichen, ist, die Befehlszeile zwischen jedem Schreiben der vollständigen Anzeige zu löschen.
Im obigen Beispiel habe ich nach dem Befehl clear
gesucht. Unter Windows lautet der Shell-Befehl zum Löschen der Befehlszeile cls
, aber Sie werden keine ausführbare Datei dafür finden, da er Teil des internen Codes von command.com
ist.
Die gute Nachricht ist, dass das Erfassen der String-Ausgabe des Linux-Befehls clear
diese Escape-Code-Sequenz ergibt: \e[3J\e[H\e[2J
. Ich und andere haben dies unter Windows, Mac und Linux getestet, und es tut genau das, was wir wollen: den Bildschirm löschen, um ihn neu zu zeichnen.
Dieser Escape-String hat drei verschiedene Aktionen, die er ausführt:
CLEAR = (ERASE_SCOLLBACK = "\e[3J") + (CURSOR_HOME = "\e[H") + (ERASE_DISPLAY = "\e[2J")
Es ist jedoch besser, eine Befehlszeilenbibliothek zu verwenden, als selbst Escape-Codes zu schreiben. Aber dies ist durchaus erwähnenswert.
Testen von STDIN/STDOUT/STDERR
Bei allem, was in etwas wie STDOUT oder STDERR schreibt, wäre es am klügsten, eine interne Variable in Ihrem UI-Ruby-Objekt zu erstellen, die mit einem optionalen Parameter an new
geändert werden kann. Wenn Ihr Programm also normalerweise läuft, ist der Wert STDOUT
. Wenn du aber Tests schreibst, gibst du StringIO.new
ein, gegen das du leicht testen kannst.
Wenn du versuchst, IO von einem Befehl zu lesen, der außerhalb von Ruby ausgeführt wird, ist das Testen etwas komplizierter. Sie müssen wahrscheinlich in Open3.popen3 schauen, um jeden IO-Stream STDIN, STDOUT und STDERR zu behandeln.
Benutzung von Bash-Befehlen
Bash ist für Windows (WSL) verfügbar! Das bedeutet, dass Sie Zugriff auf Bash-Befehle für die große Mehrheit der weltweit verwendeten Betriebssysteme haben. Das eröffnet mehr Möglichkeiten für Ihr eigenes Kommandozeilentool.
Es ist typisch, Befehle durcheinander zu „pipen“, also die Ausgabe eines Befehls als Stream an den nächsten zu senden. Wenn Sie das wissen, sollten Sie vielleicht darüber nachdenken, die Unterstützung für Streaming-Eingaben hinzuzufügen oder sich überlegen, wie Sie Ihre Ausgaben besser für andere Kommandozeilen-Tools formatieren können.
Hier ist ein Beispiel für eine Regex-Substitution mit dem Bash-Befehl sed
, dessen Eingabe von echo gepiped wird:
echo hello | sed "s/ll/ck n/g"
Das ergibt heck no
. Je mehr Sie über die Bash lernen, desto besser sind Sie in der Lage, Dinge auf der Kommandozeile zu erledigen und desto besser sind Sie gerüstet, um bessere eigene Kommandozeilen-Tools zu schreiben.
Zusammenfassung
Es gibt viel zu lernen, wenn es darum geht, eigene Kommandozeilen-Tools zu schreiben. Je mehr Situationen Sie berücksichtigen müssen, desto komplexer werden die Dinge.
Nehmen Sie sich die Zeit, die Ihnen zur Verfügung stehenden Werkzeuge zu erlernen, und Sie werden viele Vorteile ernten, von denen Sie nicht wussten, dass Sie sie vermissen. Ich wünsche Ihnen alles Gute!