The command-line interface (CLI) is the inner world of software development. From the shell, we have direct access to all the operating system’s capabilities, and with that comes the power to compose and orchestrate all aspects of the software. Many tools and frameworks incorporate command lines. Not only that, but the command prompt is the root magic of working with software systems; it’s the home of near unlimited possibilities. In this article, we’ll take a tour of building sophisticated interactive command-line interface (CLI) applications and REPLs (read–eval–print loops, or interactive shells) in Java. We’ll set up a basic demo application in Java and use the JLine and ConsoleUI libraries to add the features that we need.The Java-based REPLOur demonstration is based on a theoretical application that examines a software project’s working directory and gathers information about the projects there. The application also is able to create new projects in the directory. The example application will start a REPL that accepts two commands, describe and create, which can be tab-completed. The describe command will list the folder hierarchy of the working directory with color coding (using paging if necessary), while create initiates an interactive menu that lets the user choose what kind of project to create—Java, JavaScript, or Python. If it’s a Java application, we’ll allow a multi-select of additional features the user can add (database or REST API) that will let us see a nested menu. We’ll just use these features to explore the JLine capabilities, rather than actually implementing them.The demo applicationFor this tour, you’ll need a Java JDK and Maven installed. We’ll start by creating a fresh application with a Maven archetype, like what’s shown in Listing 1.Listing 1. Create a new application with Maven
mvn archetype:generate -DgroupId=com.infoworld -DartifactId=jline3 -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
Maven will use these commands to lay out a new project for us. Before we go any further, let’s also add all the dependencies we’ll need, and also set the Java version to 11 (any version from Java 8 forward should work), as I’ve done in Listing 2. This applies to the pom.xml file in the project root (leave the rest of the pom.xml as-is).Listing 2. Add dependencies and set the Java version
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline</artifactId>
<version>3.16.0</version>
</dependency>
<dependency>
<groupId>org.fusesource.jansi</groupId>
<artifactId>jansi</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline-terminal-jansi</artifactId>
<version>3.20.0</version>
</dependency>
</dependencies>
Next, let’s modify the main class in src/main/java/com/infoworld/App.java to start a REPL loop. Modify App.java using the code in Listing 3.Listing 3. A simple REPL loop
package com.infoworld;
import org.jline.reader.*;
import org.jline.reader.impl.*;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.impl.completer.*;
import org.jline.terminal.TerminalBuilder;
import org.jline.terminal.Terminal;
import java.io.IOException;
import java.util.*;
public class App {
public static void main(String[] args) throws IOException {
Terminal terminal = TerminalBuilder.terminal();
LineReader reader = LineReaderBuilder.builder()
.terminal(terminal)
.completer(new StringsCompleter(“describe”, “create”))
.build();
while (true) {
String line = reader.readLine(“> “);
if (line == null || line.equalsIgnoreCase(“exit”)) {
break;
}
reader.getHistory().add(line);
System.out.println(“You said: ” + line);
}
}
}
Listing 3 creates a very simple program that watches for lines of user input and echoes them back. To that, I added a “completer,” which holds the two commands we support, describe and create. That means when the user is typing at the prompt, they can tab to complete these commands. Tabbing twice will offer a menu with the available commands. JLine has made this very easy with the fluent-style .completer(new StringsCompleter(“describe”, “create”)) method call. JLine has several built-in completers, in addition to Strings, and you can also build your own.At heart, the REPL is an infinite while loop, which breaks when the user enters exit.You can test it out by running the Maven exec:java command shown in Listing 4.Listing 4. Run the program with Maven
mvn clean package exec:java -Dexec.mainClass=com.infoworld.App
You’ll get the carrot prompt, the echo response, and the tab-completion commands.Handling REPL commandsNow that we have the echo REPL working with auto-complete, let’s actually handle the commands. We’ll do this with typical Java, by comparing the string entered with the commands and calling methods for each. For now, create won’t do anything, but we’ll implement the logic to output the directory hierarchy, as shown in Listing 5.Listing 5. REPL with the describe command
package com.infoworld;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;
import org.jline.reader.LineReader;
import org.jline.reader.impl.completer.StringsCompleter;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.impl.DefaultParser;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
public class App {
public static void main(String[] args) throws IOException {
Terminal terminal = TerminalBuilder.terminal();
LineReader reader =
LineReaderBuilder.builder()
.terminal(terminal)
.completer(new StringsCompleter(“describe”, “create”))
.parser(new DefaultParser())
.build();
while (true) {
String line = reader.readLine(“> “);
if (line == null || line.equalsIgnoreCase(“exit”)) {
break;
}
reader.getHistory().add(line);
if (line.equalsIgnoreCase(“describe”)) {
Path path = Paths.get(“.”);
System.out.println(getDirectoryHierarchy(path));
} else if (line.equalsIgnoreCase(“create”)) {
System.out.println(“TBD”);
} else {
System.out.println(“Unknown command: ” + line);
}
}
}
public static String getDirectoryHierarchy(Path path) {
StringBuilder sb = new StringBuilder();
try (Stream<Path> paths = Files.walk(path)) {
paths.sorted()
.forEach(
p -> {
int depth = path.relativize(p).getNameCount();
for (int i = 0; i < depth; i++) {
sb.append(” “);
}
if (p.toFile().isDirectory()) {
sb.append(“/”);
}
sb.append(p.getFileName()).append(“n”);
});
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
}
Now when you run the application, if you enter the describe command, you’ll see an indent-formatted list of the working directory. The work of building that string up happens in getDirectoryHierarchy(). That method uses normal Java from the java.nio.file package to walk the directory and output each file and directory, indenting a space for each level of directory we go down. This work is mainly done with path.relativize(p).getNameCount(), which says: from my current path (.) give me the relative path to the current one (e.g., ./src/main/java). The getNameCount() just counts the number of names in that path—three, in this case. For each name, we add a space.
Previous
1
2
Page 2
When we run this code now, we’ll get output like what’s shown in Listing 6.Listing 6. Output of the describe command
> describe
/.
pom.xml
/src
/main
/java
/com
/infoworld
App.java
…
That looks a little dull, so let’s spruce it up, as shown in Listing 7. Listing 7. The getDirectoryHierarchy() with colors
public static String getDirectoryHierarchy(Path path) {
StringBuilder sb = new StringBuilder();
try (Stream<Path> paths = Files.walk(path)) {
paths.sorted()
.forEach(
p -> {
int depth = path.relativize(p).getNameCount();
for (int i = 0; i < depth; i++) {
sb.append(” “);
}
String fileName = p.getFileName().toString();
if (p.toFile().isDirectory()) {
fileName = “3[32m” + “/” + fileName + “3[0m”;
} else {
fileName = “3[34m” + fileName + “3[0m”;
}
sb.append(fileName).append(“n”);
});
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
Now the directories will be green and files will be blue, as shown in Figure 1. IDG
Figure 1. A directory listing with color
The color is relying on the Jansi library dependency that we added in pom.xml, which supports escape codes to denote the font color at the terminal. We use the escape codes 3[32m and 3[34m to change colors for a moment, then reset to the default with 3[0m. The 3 is the “escape character,” which tells the system that a control code follows, which tells the terminal how to format the text. The [32m and [34m are green and blue colors, respectively, and [0m clears the formatting. The Jansi library lets your Java program use all these codes in a cross-platform way, without worrying about the underlying operating system. You can learn more about the history of ANSI escape codes here, and more about the Jansi library here. (Jansi’s motto is “Eliminating boring console output.”)Create and add user menusNow that we have the describe command working, let’s take a crack at create. The first thing we need to do is offer a choice to the user of what kind of project to create: Java, JavaScript, or Python. If the user picks Java, we’ll let them specify a couple of features to add: database and REST API. Listing 8 has a simple version of using JLine to handle the create command.Listing 8. Modified REPL with the create command
package com.infoworld;
// …other imports remain the same
import java.util.stream.Stream;
// …
} else if (line.equalsIgnoreCase(“create”)) {
String choice = reader.readLine(“What kind of project do you want to create? (java, javascript, python) “);
if (choice.equalsIgnoreCase(“java”)) {
createJavaProject(reader);
} else if (choice.equalsIgnoreCase(“javascript”)) {
System.out.println(“TBD”);
} else if (choice.equalsIgnoreCase(“python”)) {
System.out.println(“TBD”);
} else {
System.out.println(“Unsupported choice: ” + choice);
}
} else {
System.out.println(“Unknown command: ” + line);
}
}
}
public static void createJavaProject(LineReader reader) {
boolean addDatabase = reader.readLine(“Add Database Support? (y/n) “).equalsIgnoreCase(“y”);
boolean addRest = reader.readLine(“Add REST API? (y/n) “).equalsIgnoreCase(“y”);
//TODO: Create project here
System.out.println(“Java project created with Database: ” + addDatabase + “, REST: ” + addRest);
}
//…
}
Listing 8 is pretty straightforward. We prompt for the type of project to create, and if the user enters java we send them to the createJavaProject() method. Notice we share the reader object as an argument—the JLine project encourages reusing the reader (and in fact testing reveals that spawning a new reader while another is running causes a dumb terminal to be created, at least on Debian). The createJavaProject() chains together two questions, about adding a database and adding a REST API. We just output the user’s selections. This use of the create command demonstrates just one approach to handling multi-select functionality. The interaction looks like what’s shown in Listing 9.Listing 9. Using the create command to chain together two questions
> create
What kind of project do you want to create? (java, javascript, python) java
Add Database Support? (y/n) y
Add REST API? (y/n) y
Java project created with Database: true, REST: true
> exit
This works, but it’s a bit primitive. With the ConsoleUI project, we can implement some much nicer CLI-style menus. ConsoleUI supports the whole range of UI widgets like select, multiselect, and radio buttons with arrow-key navigation and ANSI icons. We’ll just get a quick flavor of it here by implementing the create command with ConsoleUI.First, we add the ConsoleUI dependency, as shown in Listing 10.Listing 10. Adding the ConsoleUI dependency
<dependency>
<groupId>de.codeshelf.consoleui</groupId>
<artifactId>consoleui</artifactId>
<version>0.0.13</version>
</dependency>
Listing 11 gives a quick example of adding the create menus. ConsoleUI gives you a largely fluent API that you can chain together to form the prompts you need. To access the results of the user’s selections, you have to know how the framework models them (details here).Listing 11. Fancy menus
package com.infoworld;
//…
// consoleui
import de.codeshelf.consoleui.elements.ConfirmChoice;
import de.codeshelf.consoleui.prompt.ConfirmResult;
import de.codeshelf.consoleui.prompt.ConsolePrompt;
import de.codeshelf.consoleui.prompt.PromtResultItemIF;
import de.codeshelf.consoleui.prompt.builder.PromptBuilder;
import java.io.IOException;
import java.util.HashMap;
import static org.fusesource.jansi.Ansi.ansi;
import org.fusesource.jansi.AnsiConsole;
// …
public static void createCommand() {
AnsiConsole.systemInstall();
System.out.println(ansi().render(“@|red,italic Hello|@ @|green World|@n@|reset Welcome to the project creator.|@”));
try {
ConsolePrompt prompt = new ConsolePrompt();
PromptBuilder promptBuilder = prompt.getPromptBuilder();
promptBuilder.createListPrompt().name(“projectType”).message(“What kind of project do you want to create? (java, javascript, python) “) .newItem().text(“java”).add().newItem(“javascript”).text(“javascript”).add()
.newItem(“python”).text(“python”).add().addPrompt();
HashMap<String, ? extends PromtResultItemIF> result = prompt.prompt(promptBuilder.build());
String projectType = ((de.codeshelf.consoleui.prompt.ListResult)result.get(“projectType”)).getSelectedId();
System.out.println(“result = ” + ((de.codeshelf.consoleui.prompt.ListResult)result.get(“projectType”)).getSelectedId());
System.out.println(“Building a new project: ” + projectType);
if (projectType== “java”) {
prompt = new ConsolePrompt();
promptBuilder = prompt.getPromptBuilder();
promptBuilder.createCheckboxPrompt()
.name(“features”)
.message(“Please select features to add:”)
.newSeparator(“Databases:”)
.add()
.newItem().name(“mongo”).text(“MongoDB”).add()
.newItem(“mysql”).text(“MySQL”).add()
.newItem(“db2”).text(“DB2”).disabledText(“Sorry, discontinued.”).add()
.newSeparator().text(“Rest API”).add()
.newItem(“spring-rest”).text(“Spring REST”).check().add()
.newItem(“micronaut”).text(“Micronaut”).add()
.addPrompt();
result = prompt.prompt(promptBuilder.build());
System.out.println(“result = ” + result);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
TerminalFactory.get().restore();
} catch (Exception e) {
e.printStackTrace();
}
}
}
//…
}
Now you’ll see a very nice console UI, like the one shown in Figure 2. IDG
Figure 2. The updated console UI.
ConclusionThis was a quick tour demonstrating some of the features of the JLine and ConsoleUI libraries. Together, they make many powerful CLI and REPL features simple to implement in Java.
Next read this:
Source: infoworld.com