Zu Befehl: CLIs mit Node.js und Commander
Im Gegensatz zu Anwendungen mit einer grafischen Benutzeroberfläche (GUI) sind CLIs (Command Line Interfaces) Programme, die nicht nur in einem Terminal gestartet, sondern typischerweise auch gesteuert werden. Dabei kann der Endnutzer Argumente an das Programm übergeben, um dieses via Optionen zu konfigurieren (z. B. --debug
) oder verschiedene Aktionen durchzuführen (z. B. start <service>
).
In diesem Artikel wird zunächst im Ansatz gezeigt, wie CLIs ohne Hilfsmittel erstellt werden können. Hierfür wird Node.js verwendet, wodurch JavaScript auch ohne Browser ausgeführt werden kann. Anschließend wird Commander.js vorgestellt. Mit dieser Bibliothek können Argumente im Handumdrehen geparst werden, wodurch die Entwicklung von CLIs erheblich vereinfacht wird.
Terminologie
In diesem Artikel wird zwischen folgenden Begriffen unterschieden:
- Argument: Eine Zeichenkette, die beim Aufruf an ein Programm übergeben wird (meist getrennt durch Leerzeichen)
- Command: Ein Argument, durch das eine bestimmte Teil-Funktionalität/Aktionen des Programms ausgeführt wird (z. B.
start
oderstop
) - Option: Ein Argument, welches den Aufruf des Programms konfiguriert (beginnt meist mit
-
oder--
)
Programm-Argumente händisch parsen - ist doch gar nicht so schwer?
Das Herzstück einer CLI ist die Interpretation der Argumente, welche vom Endnutzer spezifiziert wurden. In Node.js kann man über das Array process.argv
relativ einfach auf diese Argumente zugreifen:
/* manual.js */ // Die ersten zwei Elemente sind grundsätzlich der Pfad des verwendeten Node-Interpreters und der Pfad zum ausgeführten Skript console.log(process.argv[0]); // Node-Interpreter console.log(process.argv[1]); // Skript-Pfad // Alles, was danach kommt, sind vom Endnutzer angegebene Argumente const args = process.argv.slice(2); console.log('args:', args); $ node manual.js --foo bar /usr/bin/node /tmp/cli/manual.js args: [ '--foo', 'bar' ]
Anschließend kann man die Argumente einfach mit einem for
-Loop durchlaufen und auf die Präsenz bestimmter Werte prüfen. Wieso braucht man dafür eine Bibliothek!?
Zu viele Aspekte
Nach kurzer Überlegung wird klar, dass die manuelle Entwicklung einer CLI doch nicht so einfach ist. Um die Anwendung benutzerfreundlich und intuitiv zu gestalten, müssen nämlich viele weitere Aspekte beachtet werden:
- Argumente können erforderlich oder optional sein.
- Argumente können Werte verschiedener Typen repräsentieren (String, Boolean etc.).
- Mögliche Doppelungen sollten behandelt werden.
- Dokumentation der möglichen Argumente, z. B. über
--help
-Option - Die Reihenfolge der Argumente kann einen signifikanten Unterschied machen.
- Was, wenn das Skript über einen anderen Weg ausgeführt wird? (➔ Ist es möglich, dass das erste Argument nicht immer
argv[2]
ist?) - …
CLIs mit Commander
Commander.js ist eine solche Bibliothek, welche über folgenden Befehl installiert werden kann:
$ npm install commander
Anschließend kann Commander wie folgt initialisiert werden:
/* my-cli.js */ const { program } = require('commander'); // Bestimmung verschiedener Metadaten program .name('my-cli') .description('Eine CLI mit Commander.js!') .version('1.0.0'); // Evaluiert alle Argumente und Metadaten program.parse();
Eine Ausführung dieses Skripts ohne Argumente erzeugt noch keine Ausgabe. Allerdings überprüft Commander bereits, ob falsche Optionen angegeben wurden. Zudem wird eine Hilfe-Ausgabe mit den Metadaten generiert, wenn die Option --help
angegeben wurde:
$ node my-cli.js $ node my-cli.js --debug error: unknown option '--debug' $ node my-cli.js --help Usage: my-cli [options] Eine CLI mit Commander.js! Options: -V, --version output the version number -h, --help display help for command
Optionen hinzufügen
Um dem Programm nun Optionen hinzuzufügen, kann die Funktion program.option()
verwendet werden. Darin können der Name einer Option (z. B. --debug
), deren Shortcut (-d
) und eine Beschreibung angegeben werden. Nach dem Parsen aller Argumente können die Optionen via program.opts()
ausgelesen werden:
const { program } = require('commander'); program .name('my-cli') .description('Eine CLI mit Commander.js!') .version('1.0.0'); // Optionen definieren program .option('-d, --debug', 'Aktiviert den Debug-Modus') .option('-c, --config <path>', 'Pfad zu einer Konfigurationsdatei'); // Argumente evaluieren program.parse(); // Optionen auslesen const args = program.opts(); // Überprüfen, welche Optionen spezifiziert wurden if (args.debug) { console.log('Debug-Modus aktiviert!'); } if (args.config) { applyConfigurationFromFile(args.config); } $ node my-cli.js --help Usage: my-cli [options] Eine CLI mit Commander.js! Options: -V, --version output the version number -d, --debug Aktiviert den Debug-Modus -c, --config <path> Pfad zu einer Konfigurationsdatei -h, --help display help for command $ node my-cli.js -c config.json --debug Debug-Modus aktiviert!
Commands und Actions
Die o. g. Funktionalitäten nehmen einem bereits viel Arbeit ab. Allerdings bietet Commander noch weitere Möglichkeiten, um CLIs zu erweitern. So können neben den Optionen auch sog. Commands definiert werden, welche größere alleinstehende Funktionalitäten repräsentieren. Dabei kann ein Command ein Executable ausführen, welches sich im Dateisystem befindet (z. B. ein Bash-Skript oder eine kompilierte Binary). Wichtig bei Skripten ist hierbei nur, dass die benötigten Berechtigungen und im Skript eine Shebang (z. B. #!/bin/bash
) vorhanden sind. Alternativ kann dem Command innerhalb der Anwendung eine Action hinzugefügt werden:
program .command('foo', 'Führt ein Bash-Skript aus', { executableFile: 'foo.sh' }) .command('bar', 'Führt ein Node.js-Skript aus', { executableFile: 'bar.js' }); program .command('start <service>') // Argument ist <erforderlich> .description('Startet einen Service') .option('-r, --restart', 'Startet den Service neu, falls er bereits läuft') .action((service, args) => console.log(`Starte Service: ${service} ${args.restart ? 'neu' : ''}`)); program .command('stop [service]') // Argument ist [optional] .description('Stoppt einen oder alle Services') .action((service) => service ? console.log('Stoppe Service:', service) : console.log('Stoppe alle Services')); program.parse();
Standardmäßig werden Executables im Verzeichnis des ausgeführten Node-Skripts gesucht. Über .executableDir()
kann jedoch ein anderes Verzeichnis angegeben werden.
# Skripte erstellen $ echo -e "#!/bin/bash\necho 'Hello World from Bash!'" > foo.sh $ echo -e "#!/usr/bin/env node\nconsole.log('Hello World from Node.js!');" > bar.js # Skripte ausführbar machen $ chmod 744 foo.sh bar.js $ node my-cli.js foo Hello World from Bash! $ node my-cli.js bar Hello World from Node.js! $ node my-cli.js start ordix Starte Service: ordix # Argumente (-c config.json) zwischen Command (stop) und Wert des Commands (ordix) werden korrekt erkannt! $ node my-cli.js stop -c config.json ordix Stoppe Service: ordix $ node my-cli.js stop Stoppe alle Services
Fazit
Bei der Erstellung von Anwendungen kommt es oft vor, dass die Endnutzer Optionen auswählen oder bestimmte Werte festlegen müssen. Das Ziel sollte dabei stets eine benutzerfreundliche und fehlerfreie Verarbeitung der Argumente sein. Dazu müssen verschiedene Aspekte beachtet werden, wie z. B. die Dokumentation von Argumenten, Behandlung von Doppelungen u. v. m.
Erstellt man solche CLIs in Node.js, nimmt einem die Bibliothek Commander.js jedoch fast die ganze Arbeit ab. Dadurch wird viel Zeit gewonnen, welche für die Entwicklung der eigentlichen Funktionalität einer Anwendung aufgewendet werden kann.
Seminarempfehlungen
LINUX/UNIX GRUNDLAGEN FÜR EINSTEIGER BS-01
Zum SeminarSHELL, AWK UND SED P-UNIX-01
Zum SeminarTYPESCRIPT GRUNDLAGEN E-TYPSC-01
Zum SeminarJunior Consultant bei ORDIX
Bei Updates im Blog, informieren wir per E-Mail.
Kommentare