Unscharf: Den Nagel im Heuhaufen suchen

Shell-Skripte zu schreiben ist eine der einfachsten Aufgaben unter Node.js und io.js, Module wie commander und buntstift übernehmen die schwierigen Aufgaben: das Zerlegen und Auswerten von Parametern und das Formatieren der Konsolenausgabe. Doch wie steht es um das Erkennen von unbekannten Kommandos?

In Pocket speichern vorlesen Druckansicht
Lesezeit: 5 Min.
Von
  • Golo Roden
Inhaltsverzeichnis

Shell-Skripte zu schreiben ist eine der einfachsten Aufgaben unter Node.js und io.js, Module wie commander und buntstift übernehmen die schwierigen Aufgaben: Das Zerlegen und Auswerten von Parametern und das Formatieren der Konsolenausgabe. Doch wie steht es um das Erkennen von unbekannten Kommandos?

Ein einfacher Taschenrechner für die Konsole, der beliebig viele Zahlen addieren kann, ist auf Basis der Array.reduce-Funktion rasch geschrieben. Das Rahmengerüst der Anwendung bilden die Module commander und buntstift:

#!/usr/bin/env node

'use strict';

var buntstift = require('buntstift'),
program = require('commander');

program
.version('0.0.1');

program
.command('add <numbers...>')
.description('return the sum of the operands')
.action(function (numbers) {
buntstift.success('The sum is {{sum}}.', {
sum: numbers.reduce(function (sum, value) {
return (sum | 0) + (value | 0);
})
});
});

program
.parse(process.argv);

Durch die Verwendung der command-Funktion ist das Schlüsselwort add als eigenständiges Kommando innerhalb der Anwendung registriert, weshalb sich das Berechnen der Summe direkt auf der Kommandozeile aufrufen lässt:

$ ./calculator.js add 23 42

Auch die Ausgabe einer ausführlichen Hilfe ist möglich, indem die Anwendung mit dem automatisch von commander erzeugten Parameter --help aufgerufen wird:

$ ./calculator.js --help

Doch was, wenn der Anwendung weder ein Kommando noch der --help-Parameter übergeben werden? In diesem Fall bleibt die Konsole leer, die Anwendung gibt schlichtweg gar nichts aus:

$ ./calculator.js

Abhilfe schafft das Abfragen des Arrays process.argv, das außer den auf der Kommandozeile übergebenen Werten auch den Namen der Runtime und den Pfad des aufgerufenen Skripts enthält. Wird das Skript ohne Parameter aufgerufen, enthält das Array dementsprechend nur zwei Werte.

In dem Fall genügt es daher, die Hilfe auszugeben und die Anwendung zu beenden. Der Aufruf der program.help-Funktion beendet die Anwendung implizit:

if (process.argv.length === 2) {
program.help();
}

Etwas komplizierter wird die Angelegenheit, wenn auf falsch eingegebene Kommandos reagiert werden soll. Prinzipiell lässt sich dafür das *-Ereignis verwenden, das commander bei unbekannten Kommandos auslöst. Im einfachsten Fall kann man die Anwendung abbrechen und beispielsweise die Hilfe ausgeben:

program
.on('*', function (argv) {
program.help();
});

Damit das Vorgehen einwandfrei funktioniert, ist allerdings darauf zu achten, diese Zeilen erst nach dem Registrieren der einzelnen Kommandos, aber vor dem Parsen der Kommandozeilenparameter durchzuführen.

Schön wäre allerdings, dem Anwender eine bessere Hilfestellung zu bieten als lediglich die kommentarlose Ausgabe des Hilfetexts. Beispielsweise wäre denkbar, dem Anwender bei einem Schreibfehler das korrigierte Kommando vorzuschlagen.

Zu diesem Zweck lässt sich das Modul findsuggestions nutzen, das aus einer Liste von Begriffen und einem zu vergleichenden Begriff eine Rangliste anhand der Ähnlichkeit der Begriffe erstellt. Auf diese Weise ist es möglich, zu einem falsch geschriebenen Kommando das korrekte zu ermitteln, zumindest mit einer gewissen Wahrscheinlichkeit.

Dazu benötigt man allerdings zunächst das aufgerufene Kommando und eine Liste aller gültigen Kommandos. Das erste lässt sich aus dem Parameter der ereignisbehandelnden Funktion ermitteln: In dem übergebenen Array ist das Kommando der erste enthaltene Wert, also argv[0].

Schwieriger ist es, an die Liste aller gültigen Kommandos zu gelangen. Innerhalb der Funktion lässt sich auf diese Liste zwar mit this.commands zugreifen, allerdings erhält man dann ein Array von Objekten, die jeweils über eine _name-Eigenschaft verfügen, in der der eigentliche Kommandoname enthalten ist.

Um die Namen zu extrahieren, bietet sich der Einsatz der pluck-Funktion aus dem Modul lodash an. Der Aufruf

_.pluck(this.commands, '_name')

liefert ein Array mit den Werten der _name-Eigenschaften aller Objekte aus dem Array this.commands. Fügt man die Teile nun zusammen und übergibt sie an die findSuggestions-Funktion, erhält man eine nach Wahrscheinlichkeiten sortierte Liste mit Übereinstimmungen:

var suggestedCommands = findSuggestions({
for: argv[0],
in: _.pluck(this.commands, '_name')
});

Der erste Vorschlag entspricht jenem mit der höchsten Wahrscheinlichkeit, weshalb sich eine entsprechende Fehlermeldung leicht formulieren lässt:

buntstift.error('Unknown command {{given}}, did you mean {{suggested}}?', {
given: argv[0],
suggested: suggestedCommands[0].suggestion
});
buntstift.exit(1);

Ruft man die Anwendung nun fehlerhaft auf, erhält man eine anwenderfreundliche und hilfreiche Fehlermeldung, die zwar nicht auf die Nadel, dafür aber zumindest auf den Nagel im Heuhaufen deutet:

$ ./calculator.js adt 23 42
Unknown command adt, did you mean add?

Selbstverständlich hat der von dem Modul findsuggestions verwendete Levenshtein-Algorithmus seine Grenzen, zum Erkennen einfacher Schreibfehler ist er allerdings bestens geeignet.

tl;dr: Die Module commander und buntstift vereinfachen das Schreiben von Shell-Skripten in Node.js. Für eine komfortable Fehlerbehandlung bei Schreibfehlern lässt sich das Modul findsuggestions verwenden, das auf dem Levenshtein-Algorithmus basiert. ()