Bild: Nicolas Marchildon - http://www.flickr.com/photos/elecnix/5500403020/

Grunt.js: Command-Line-Argumente übergeben

 

Über den TaskRunner Grunt muss ich wohl nicht mehr viel schreiben. Wer noch nichts davon gehört hat, dem sei diese Grunt-Einführung nahegelegt.

In diesem Beitrag möchte ich daher eine Funktionalität beleuchten, die nicht so präsent in den Task-Beschreibungen und Grunt-Tutorials ist: das sehr einfache Übergeben von Command-Line-Argumenten. Node.js bietet dafür zwar den Array process.argv (Beschreibung), doch die Handhabung ist etwas umständlich. Im Grunt-Kontext ist man da mehr Komfort gewöhnt.

Und in der Tat bietet das Grunt-API was wir brauchen, doch fangen wir vorne an.

Das Szenario

Angenommen, wir haben ein Projekt, welches aus eigenständigen Sub-Modulen besteht. Diese sind eines eigenen Projekts nicht würdig und so zahlreich, dass das Linten und Testen all dieser Module eine erhebliche Menge Zeit in Anspruch nimmt. Die Sub-Module müssen sich im Entwicklungsbetrieb also einzeln linten und testen lassen. Gleichzeitig soll die Gruntfile.js nicht vor Sub-Tasks überlaufen.

CLI-Arguments to the rescue!

Die Tasks, die sich auf einzelne Sub-Module beziehen, sollen also dynamisch angelegt sein. Eine Spezifizierung findet über die CLI-Eingabe statt. Dafür bietet Grunt zwei Möglichkeiten.

Flags mit grunt.option auslesen

Beim Starten eines Grunt-Tasks, können beliebig viele Flags gesetzt werden. Dabei gibt es Grunt-interne, wie --verbose oder --debug. Man kann jedoch auch eigene setzen und mit der grunt.option-Methode auslesen.

Ein einfaches Flag ist dabei ein Bool'scher Parameter. Übergibt man noch einen Wert, wird dieser als String ausgelesen:

/*
 * Mit folgender CLI-Eingabe ...
 * 
 * $ grunt --foo
 *
 * erhalten wir im Gruntfile ...
**/
console.log(typeof grunt.option('foo'));
// => boolean

console.log(grunt.option('foo'));
// => true

/*
 * Uebergeben eines Wertes ...
 *
 * $ grunt --foo bar
**/
console.log(typeof grunt.option('foo'));
// => string

console.log(grunt.option('foo'));
// => 'bar'

Diese Möglichkeit kann genutzt werden, um den Namen des relevanten Sub-Moduls als Flag zu übergeben und den Pfad in der Gruntfile.js dynamisch zu setzen:

/*
 * Folgender CLI-Command flattert rein:
 *
 * $ grunt jshint:mod --module foo
 *
 * In der Grunt-Konfiguration wird er
 * folgendermassen verarbeitet:
**/
grunt.initConfig({

    /*
     * Das relevante Modul wird als Config-
     * Property gespeichert, um komfortabel
     * mit dem Grunt-Templating darauf zu-
     * greifen zu koennen.
    **/
    module: typeof grunt.option('module') === 'string'
        ? grunt.option('module')
        : '',

    jshint: {
        mod: {
            files: ['path/to/<%= module %>/*.js']
        }
    }

});

Es wird geprüft, ob das --module-Flag gesetzt und ein Wert übergeben wurde. Ist das nicht der Fall, wird der module-Eigenschaft ein leerer String zugewiesen. JSHint wird in dem Fall keine Dateien finden und abbrechen. Wird ein Sub-Modul definiert, sucht JSHint am entsprechenden Ort und lintet alle JS-Dateien.

Dynamische Sub-Tasks definieren

Die zweite Möglichkeit, CLI-Argumente entgegen zu nehmen, nutzt die Möglichkeit von Grunt, Custom-Tasks anzulegen. Diese können beliebige Sub-Tasks verarbeiten. Die Eingabe findet über die Doppelpunkt-Verknüpfung statt.

Um also das gleiche Resultat wie im vorherigen Beispiel zu erreichen, können wir folgendes machen:

/*
 * Anlegen eines Custom-Task `lint`, welcher als
 * Sub-Task das relevante Sub-Modul entgegen nimmt.
**/
grunt.registerTask('lint', 'Lintet ein Sub-Modul', function (module) {

    /*
     * Pruefen, ob ein Sub-Modul uebergeben wurde.
     * Falls nicht, ist der Spass an dieser Stelle
     * auch schon wieder vorbei.
    **/
    if ( !module || typeof module !== 'string' ) {
        grunt.fail.warn('Bitte ein Sub-Modul definieren.');
    }

    /*
     * Setzen des `module`-Wertes im Config-Objekt.
    **/
    grunt.config.set('module', module);

    /*
     * Starten des JSHint-Tasks
    **/
    grunt.task.run('jshint:mod');        

});

Mit folgendem CLI-Befehl können wir den Task starten:

$ grunt lint:foobar

Dies lintet alle JS-Dateien des Sub-Modules 'foobar'. Der Befehl ist äquivalent zu:

$ grunt jshint:mod --module foobar

In diesem Falle haben wir mit der zweiten Variante also wenig gewonnen. Seine Stärke kann die zweite Lösung ausspielen, wenn es um die Verknüpfung von Tasks geht.

Das ganze live erleben

Um ein Hands-on zu ermöglichen, gibt es auf GitHub eine Demo. Diese enthält zwei Beispiel-Sub-Module nebst Tests und zwei Tasks, die aufgerufen werden können: JSHint und Karma-Testrunner.

Installieren der Demo

Um die Demo zu ziehen, begeben wir uns in die Konsole und geben folgendes ein:

$ git clone https://github.com/neuwaerts-de/grunt-options-demo
$ cd grunt-options-demo
$ npm install

Dies klont das zugehörige Repository auf deinen Computer und lädt anschließend die benötigten Dependencies. Ein Blick in die Gruntfile.js lohnt, da diese üppig kommentiert ist und einen guten Einblick in den Umgang mit CLI-Argumenten im Grunt-Kontext gibt.

Nutzung der Demo

Folgende Grunt-Tasks stehen zur Verfügung:

# Alle JS-Dateien linten
$ grunt jshint:all

# JS-Dateien eines einzelnen Sub-Moduls linten
$ grunt jshint:mod --module <module-name>

# Alle Tests durchfuehren
$ grunt karma:all

# Tests eines einzelnen Sub-Modules durchfuehren
$ grunt karma:mod --module <module-name>

# Watch-Task fuer ein einzelnes Sub-Modul starten
$ grunt edit:<module-name>

Letzterer Befehl demonstriert die Nutzung von dynamischen Sub-Tasks.

Abschließende Worte

In den meisten Fällen wird es sicherlich reichen, Sub-Tasks anzulegen. Sollte man sich jedoch einmal in einer Situation wiederfinden, die oben beschriebenem Szenario gleicht, ist es gut zu wissen, dass sich Grunt auch mithilfe von CLI-Argumenten parametrisieren lässt.

Für Fragen und Anmerkungen sei auf die Kommentar-Funktion und twitter verwiesen. Sollte dir etwas bei der Demo nicht gefallen, kannst du entweder ein "Issue" öffnen, oder dir den Code schnappen, den Schnitzer ausbügeln und einen "Pull Request" starten.