Anwendungen für die Kommandozeile wirken heutzutage häufig antiquiert. Ein Grund hierfür ist die nur Text basierte, häufig schlicht wirkende Ausgabe. Außerdem muss der Benutzer die – manchmal doch komplexe – Aufrufsyntax lernen, um die Anwendung nutzen zu können.

Doch trotz dieser beiden Nachteile sind Anwendungen für die Kommandozeile für bestimmte Anwendungsfälle die beste Lösung. Sie lassen sich super in automatisierte Batch-Jobs einhängen, um Transformationen oder Prüfungen durchzuführen. Zudem ist die Nutzung der Maus nicht nötig. Gerade Powernutzer, die häufig ausschließlich mit der Tastatur arbeiten, gelangen so schnell und effektiv zum Ziel.

Dieser Artikel stellt im Folgenden die Bestandteile einer Kommandozeilenanwendung vor und zeigt, wie man diese in Java benutzt. Dabei sind das Vorhandensein einer passenden JVM zur Ausführung und die langsame Startzeit der JVM zwar zwei Nachteile gegenüber anderen häufiger genutzten Sprachen wie Go, Python oder Ruby, aber nicht zwangsweise ein K.O.-Kriterium.

Eingaben

Die Eingabe von Daten in eine Kommandozeilenanwendung erfolgt über die Standardeingabe. Diese ist ein Eingabestrom, der in die Anwendung geleitet wird und dort konsumiert werden kann. Java stellt hierzu in der Klasse java.lang.System den InputStream in zur Verfügung. In Listing 1 lesen wir solange alle Zeilen der Standardeingabe ein, bis diese leer ist, und merken uns diese dabei in einer Liste.

package cli;

import java.io.*;
import java.util.*;

public class Cat {
    public static void main(String[] args) throws IOException {
        final List<String> lines = new ArrayList<>();
        try (BufferedReader in =
                new BufferedReader(new InputStreamReader(System.in))) {
            String line = null;
            while ((line = in.readLine()) != null) {
                lines.add(line);
            }
        }
    }
}
Listing 1: Auslesen der Standardeingabe

Ein Weg, um nun Daten in die Standardeingabe zu bekommen, besteht darin, die Anwendung einfach zu starten. Diese läuft anschließend solange und wartet auf Eingaben des Benutzers, bis dieser das Steuerzeichen für das Dateiende (EOF) sendet (siehe Listing 2).

$ java -cp target/cli.jar cli.Cat ABC
DEF
GHI
$
Listing 2: Interaktive Standardeingabe

Alternativ können auch komplette Dateien in die Standardausgabe geleitet werden. Die Anwendung verarbeitet diese dann, ohne auf weitere Eingaben des Benutzers zu warten (siehe Listing 3).

$ java -cp target/cli.jar cli.Cat < pom.xml
$
Listing 3: Komplette Datei in Standardeingabe leiten

Ausgaben

Eine Anwendung, die nur Daten entgegennimmt, ist meistens nicht sehr sinnvoll. Von ihr berechnete Ergebnisse oder Informationen an den Benutzer sollen schließlich angezeigt werden. Hierzu stehen dem Entwickler mindestens zwei Ausgabeströme zur Verfügung.

Der erste Ausgabestrom ist die Standardausgabe. Diese sollte ausschließlich dazu genutzt werden, Ergebnisse der Anwendung auszugeben. Wie auch die Standardeingabe findet man diesen Strom in java.lang.System, jedoch als PrintStream in der Variablen out (siehe Listing 4). In diesem Fall wird einfach die zuvor gelesene Zeile direkt wieder ausgegeben (siehe Listing 5).

package cli;

import java.io.*;

public class Cat {
    public static void main(String[] args) throws IOException {
        try (BufferedReader in =
                new BufferedReader(new InputStreamReader(System.in))) {
            String line = null;
            while ((line = in.readLine()) != null) {
                System.out.println(line);
            }
        }
    }
}
Listing 4: Ausgeben von Ergebnissen auf der Standardausgabe
$ java -cp target/cli.jar cli.Cat
Hallo,
Hallo,
Welt!
Welt!
$
Listing 5: Beispiel für Ein- und Ausgabe

Der eigentliche Trick in der Benutzung von Ausgabeströmen liegt darin, dass man diese, wie auch den Eingabestrom, umleiten kann. Somit wird der Ausgabestrom einer Anwendung der Eingabestrom für die nächste, und es entsteht eine Kette von Anwendungen. Mit wenig Aufwand lassen sich so größere und komplexe Aufgaben erledigen. Um zum Beispiel herauszufinden, wie viele Zeilen, Wörter und Bytes eine Datei hat, kann die bisher entwickelte Anwendung mit der unter Linux vorhandenen Anwendung wc (word, line, character, and byte count) verknüpft werden (siehe Listing 6).

$ java -cp target/cli.jar cli.Cat < pom.xml | wc
     22     21    652
$
Listing 6: Verknüpfung von mehreren Anwendungen

Die Ausgabe gibt an, dass die Datei pom.xml 22 Zeilen, 21 Wörter und 652 Bytes enthält. Die Umleitung der Standardausgabe unserer Anwendung in die Standardeingabe von wc geschieht durch den senkrechten Strich, der im englischen Pipe genannt wird.

Neben der Umleitung in eine weitere Anwendung lässt sich die Standardausgabe auch in eine Datei umleiten. Hierzu wird das „Größer als“-Zeichen verwendet. Dabei ist zu beachten, dass die Datei überschrieben wird und der bisherige Inhalt verschwindet. Soll die neue Ausgabe lediglich an das Ende einer bestehenden Datei angehängt werden, nutzt man das Zeichen doppelt. Nach Ausführung des ersten Befehls aus Listing 7 enthält die Datei pomwc.txt lediglich eine Zeile, nach dem zweiten Befehl zwei.

$ java -cp target/cli.jar cli.Cat < pom.xml | wc > pomwc.txt
$ java -cp target/cli.jar cli.Cat < pom.xml | wc >> pomwc.txt
$
Listing 7: Verknüpfung von mehreren Anwendungen

Neben der Standardausgabe steht dem Entwickler mit der Standardfehlerausgabe noch ein zweiter Ausgabestrom zur Verfügung. Dieser sollte dazu genutzt werden, Fehler und weitere Informationen für den Benutzer auszugeben, die keine Ergebnisse sind. Somit können Ergebnisse weiterhin umgeleitet und weiterverarbeitet werden und gleichzeitig Fehler und Informationen an den Benutzer gemeldet werden.

Wie auch die Standardausgabe befindet sich in java.lang.System die Variable err, die einen PrintStream bereitstellt, der für die Standardfehlerausgabe genutzt werden kann. Listing 8 zeigt die um die Nummer der aktuell gelesenen Zeile erweiterte Ausgabe und Listing 9 zeigt den Aufruf und das Ergebnis.

package cli;

import java.io.*;

public class Cat {
    public static void main(String[] args) throws IOException {
        try (BufferedReader in =
                new BufferedReader(new InputStreamReader(System.in))) {
            String line = null;
            int lineNo = 0;
            while ((line = in.readLine()) != null) {
                System.err.println(++lineNo);
                System.out.println(line);
            }
        }
    }
}
Listing 8: Ausgeben von Informationen auf der Standardfehlerausgabe
$ java -cp target/cli.jar cli.Cat < pom.xml | wc > pomwc.txt
1
2
3
...
22
$
Listing 9: Aufruf der Anwendung mit Standardfehlerausgabe

Zu beachten ist, dass die beiden Ausgabeströme nicht miteinander synchronisiert werden und dass das Betriebssystem diese puffern kann. Dies kann dazu führen, dass zum Beispiel in unserer Anwendung die Reihenfolge von Zeilennummer und Zeileninhalt nicht immer gegeben ist. Die Reihenfolge des einzelnen Ausgabestroms ist jedoch immer korrekt.

Argumente und Optionen

Neben der Eingabe der reinen Daten und der Ausgabe von Ergebnissen und Fehlern/Informationen wird häufig noch die Möglichkeit benötigt, der Anwendung weitere Informationen zu übergeben. So ließe sich zum Beispiel konfigurierbar machen, ob die Zeilennummer auf der Standardfehlerausgabe ausgegeben werden soll oder ob die Zeilennummer vor jede Zeile des Ergebnisses geschrieben werden soll.

Für diesen Fall nutzt man Argumente und Optionen. Diese werden der Anwendung beim Start mit übergeben (siehe Listing 10). Optionen sind hierbei optionale Anweisungen für die Anwendung und bestehen aus dem Namen der Option und dem Wert, auf den diese Option gesetzt wird. Über die Jahre haben sich hierbei mehrere Schreibweisen etabliert. Die beiden gängigsten sind die Kurz- und die Langform.

$ java -cp target/cli.jar cli.Cat --debug -n < pom.xml
1
  1 <?xml version="1.0" encoding="UTF-8"?>
2
  2 <project xmlns=http://maven.apache.org/POM/4.0.0
...
21
 21 </project>
22
 22
$
Listing 10: Aufruf der Anwendung mit Optionen

Bei der Kurzform besteht der Name der Option nur aus wenigen, meistens einem, Buchstaben. Anschließend folgt der Wert, wobei Name und Wert mit Weißzeichen getrennt werden. Um also zum Beispiel den Debug-Modus anzustellen, könnte man in der Kurzform -d true angeben.

In der Langform werden die Optionsnamen in der Regel komplett ausgeschrieben. Anschließend folgt ein = und der Wert für die Option. Die Debug-Option würde in Langform also übergeben als --debug=true.

Für beide Formen gilt der Sonderfall, dass man sich im Falle der true/false-Option die Angabe eines Wertes sparen kann. Wird die Option angegeben, ist der Wert true, wenn nicht false. Sowohl –d als auch --debug setzen den Wert demnach auf true.

Argumente werden im Gegensatz zu Optionen nicht mit ihrem Namen angegeben und müssen häufig verpflichtend angegeben werden. Erlaubt eine Anwendung mehrere Argumente, so muss die Angabe dieser in der richtigen Reihenfolge erfolgen. Zudem gibt es Anwendungen, die zwingend die Angabe von Optionen vor der Angabe von Argumenten erwarten. Andere wiederum erlauben noch Optionen, nachdem die Argumente angegeben wurden.

In Listing 10 werden demnach die beiden Optionen –n und --debug auf true gesetzt. Innerhalb der Java-Anwendung werden sowohl die Optionen als auch die Argumente dem Entwickler im String-Array-Parameter der main-Methode übergeben. Dieser ist nun selber dafür verantwortlich, diese zu interpretieren (siehe Listing 11).

package cli;

import java.io.*;

public class Cat {
    public static void main(String[] args) throws IOException {
        boolean debug = false;
        boolean showLineNo = false;
        for (String arg : args) {
            switch(arg) {
                case "--debug":
                    debug = true;
                    break;
                case "-n":
                    showLineNo = true;
                    break;
            }
        }
        try (BufferedReader in =
                new BufferedReader(new InputStreamReader(System.in))) {
            String line = null;
            int lineNo = 0;
            while ((line = in.readLine()) != null) {
                ++lineNo;
                if (debug)
                    System.err.println(lineNo);
                if (showLineNo)
                    System.out.print(String.format(" %2d ", lineNo));
                System.out.println(line);
            }
        }
    }
}
Listing 11: Auswerten von Optionen und Argumenten

In diesem Falle wird das String-Array manuell ausgewertet. Dies sollte wirklich nur in trivialen Fälle erfolgen, da das korrekte Auswerten schnell sehr komplex wird. So werden zum Beispiel im Falle der Kurzform der Name der Option und der Wert in separaten Array-Einträgen übertragen und man muss selber die Beziehung zwischen den beiden Einträgen herstellen. Außerdem gibt es noch eine Vielzahl an Sonderfällen, wie beispielsweise das Zusammenfassen mehrerer boolescher Optionen.

Zum Glück gibt es jedoch bereits eine Vielzahl an Bibliotheken für Java, um das Auswerten von Optionen und Argumenten zu übernehmen. Bekannte Vertreter sind hier unter anderem Commons CLI von Apache oder args4j vom Hudson/Jenkins-Erfinder Kohsuke Kawaguchi.

Rückgabewert

Neben den Ausgabeströmen gibt es noch eine zweite Möglichkeit für eine Kommandozeilenanwendung, Feedback an den Benutzer zu übergeben, den Rückgabewert.

Mit dem Rückgabewert signalisiert die Anwendung dem Aufrufer, ob sie erfolgreich und ohne Fehler durchgelaufen ist oder nicht. Der Rückgabewert wird hierzu als ganze Zahl definiert. Ein erfolgreicher Durchlauf einer Anwendung wird hierbei mit dem Rückgabewert 0 signalisiert. Alle anderen Werte werden als Fehler interpretiert, ohne dass es einen allgemeinen Standard für die Zuordnung von Werten zu Fehlertypen gibt.

Im Standardfall beendet sich die JVM mit dem Wert 0, wenn die Anwendung am Ende der main-Methode angekommen ist und kein weiterer Thread mehr läuft. Ist eine Exception aufgetreten, die nicht behandelt wurde und somit über die main-Methode hinaus geworfen wird, gibt die JVM einen anderen Wert als 0 zurück und signalisiert somit, dass ein Fehler aufgetreten ist. Möchte man als Entwickler selber Einfluss auf den Rückgabewert nehmen, so steht die Funktion exit der Klasse java.lang.System zur Verfügung, die den gewünschten Rückgabewert als Parameter entgegennimmt. Anschließend beendet sich die JVM. Anweisungen, die hinter dem Aufruf stehen, werden nicht mehr ausgeführt.

Der Rückgabewert einer Anwendung wird in bash der Variablen ? zugewiesen und kann somit nach der Ausführung einer Anwendung ausgewertet werden. Unter DOS nutzt man die Variable errorlevel.

Fazit

Anwendungen für die Kommandozeile nutzen die Standardeingabe und Standardausgabe, um Daten entgegenzunehmen und Ergebnisse auszugeben. Die Standardfehlerausgabe wird für die Ausgabe von Fehlern und sonstigen Informationen genutzt. Weiterhin lässt sich die Anwendung über Argumente und Optionen weiter konfigurieren. Über den korrekten Rückgabewert wird mitgeteilt, ob ein Durchlauf erfolgreich war.

Alle diese Aspekte lassen sich in Java ohne größere Probleme nutzen und umsetzen. Es sind dazu keine Fremdbibliotheken erforderlich, da das JDK alle benötigten Funktionalitäten mitbringt. Jedoch kann das Auswerten von Argumenten und Optionen schnell komplex werden, und deswegen bietet sich hier die Nutzung einer Bibliothek an.