5 Minuten Lesezeit (1017 Worte)

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 oder stop)
  • 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?)

Schließlich scheint die manuelle Entwicklung einer CLI ziemlich aufwendig, da das Interpretieren der Argumente schnell sehr komplex werden kann. Zum Glück existieren jedoch diverse Bibliotheken, welche diese Aufgabe übernehmen und somit die Entwicklung von CLIs vereinfachen.

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

Junior Consultant bei ORDIX.

 

Kommentare

Derzeit gibt es keine Kommentare. Schreibe den ersten Kommentar!
Samstag, 27. April 2024

Sicherheitscode (Captcha)

×
Informiert bleiben!

Bei Updates im Blog, informieren wir per E-Mail.

Weitere Artikel in der Kategorie