Java is not traditionally used for command-line applications because of the high start-up time. Some build tools, like Scala’s sbt, will instead start a shell that takes commands. Instead of issuing commands one-by-one in, say, Bash, the sbt shell will stay active, consequently eliminating the start-up overhead. Many programming languages offer similar shells to compile and evaluate code on demand (so-called Read Eval Print Loops, or REPL for short). Even Java provides such a REPL these days.

This post uses JLine 3.x and logback-classic 1.2.x.

A classic Java library that aids in implementing such shells is JLine. Its main user-facing abstraction is a LineReader, that

And these are just some of the features.

With very little setup, you can create an application that shows the user some prompt expecting input. That input can be edited, users can press the up and down keys to navigate history, use Ctrl-combinations to skip between tokens; in other words, they get basic editing experience. When the user presses enter, JLine passes the parsed line to the application which can process the commands and start the loop again.

The virtual-terminal-based implementation of line editing is out of scope for this article. But suffice to say that all reading and printing from the terminal must be done through JLine, as can be illustrated with the following scenario: Assume that your application spawns some background threads that may print some log output. If the prompt is currently waiting for input, other threads might corrupt your terminal.

prompt> [LOG] Starting server ...
[LOG] Accepting connections ...
<user input>

Naturally, this is very confusing to the user: user input and log output get entangled in the terminal.

The reason for this is that log output in command-line applications will usually be written to the standard output or error streams. Luckily, the most popular logging frameworks can be configured to use different output mechanisms. In this example, I will explain how the above output can be straightened out using Logback, although the technique will be applicable to other frameworks.

We start by creating a custom appender class:

package test;

import ch.qos.logback.classic.layout.TTLLLayout;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
import ch.qos.logback.core.Layout;

public final class JLineAppender extends AppenderBase<ILoggingEvent> {

    private final Layout<ILoggingEvent> layout = new TTLLLayout();

    @Override
    public void start() {
        super.start();
        layout.start();
    }

    @Override
    public void stop() {
        layout.stop();
        super.stop();
    }

    @Override
    protected void append(ILoggingEvent event) {
        CLI.lineReader.printAbove(layout.doLayout(event));
    }

}

This piece of code creates a very simple Logback appender that uses the printAbove method of a JLine LineReader to print output above a prompt. Looking back at the example from above, this would lead to the following output:

[LOG] Starting server ...
[LOG] Accepting connections ...
prompt> <user input>

In essence, printAbove saves whatever the prompt looked like at the point where log output should be printed, clears the line, prints the log, and reconstructs the prompt, including user input. If the user was typing something while log was printed out, it will visually look like the prompt is moving down one line.

Note that the above code snippet assumes that the lineReader will be configured and initialized statically. In our scenario, there is a class CLI that contains the LineReader as a static variable.

Now, for the configuration of Logback:

<configuration>
    <appender name="TERMINAL" class="test.JLineAppender" />

    <root level="INFO">
        <appender-ref ref="TERMINAL" />
    </root>
</configuration>

This configuration in logback.xml will tell Logback to use the JLineAppender we have just created. As opposed to other common appenders, it does not offer the flexibility of defining our own patterns. Consult the documentation for how this could be implemented. Instead, the JLineAppender uses the TTLL layout, which at time of writing is equivalent to the pattern %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n.

To summarize: with just a little configuration effort, it is possible to combine stock Java libraries for a more pleasant command-line user experience.