I write a lot of mini apps. Each app has its own InputHandler
for input from the keyboard. It's time to make a lib for that purpose - a CommandLineInterface
.
NOTE: there exists a follow up Question here
Can you review my code on
- Design
- OOP
- Clean Code
- Naming (my bad englisch leads to bad naming)
Interface CommandLineInterpreter
Any class that implements this interface can use the CommandLineInterface
. M
is the application, which implements the CommandLineInterpreter
public interface CommandLineInterpreter<M> {
Set<Command<M>> getCommands();
Response executeCommand(String identifier, List<String> parameter);
}
Class Command
the Command
is responsible to map the command typed via CLI into an action (method-call) on your app. To execute the command the Class is generic on M
(which is your app) so you can call these app-specific methods.
public abstract class Command<M> {
private final String identifier;
public Command(String identifier) {
this.identifier = identifier;
}
public abstract Response execute(M invoked, List<String> parameter);
public String getIdentifier() {
return identifier;
}
public boolean isIdentifier(String ident) {
return identifier.equals(ident);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Command command = (Command) o;
return Objects.equals(identifier, command.identifier);
}
@Override
public int hashCode() {
return Objects.hash(identifier);
}
}
Class Response
Provides information if the execution of an command was successful or not.
public class Response {
private final String message;
private final String details;
private final Type type;
private Response(Type type, String message, String details) {
this.type = type;
this.message = message;
this.details = details;
}
public static Response fail(String message) {
return fail(message, "");
}
public static Response fail(String message, String details) {
return response(Type.FAILURE, message, details);
}
private static Response response(Type type, String message, String details) {
return new Response(type, message, details);
}
public static Response success() {
return success("ok", "");
}
public static Response success(String message) {
return success(message, "");
}
public static Response success(String message, String details) {
return response(Type.SUCCESS, message, details);
}
public boolean failed() {
return type == Type.FAILURE;
}
@Override
public String toString() {
return type.toString() + ":" + message + (details.isEmpty() ? "" : "
Details: " + details);
}
private enum Type {SUCCESS, FAILURE}
Class CommandLineInterface
This class handles the user input from the command line. It is itself a CommandLineInterpreter
and offers two basic commands: help
and exit
.
public class CommandLineInterface implements CommandLineInterpreter<CommandLineInterface> {
private static final String COMMAND_SEPARATOR = " ";
private final CommandLineInterpreter cli;
private final InputStream input;
private final PrintStream output;
private boolean isRunning = true;
public CommandLineInterface(CommandLineInterpreter<?> cli, InputStream input, PrintStream output) {
if (cli == null || hasPredefinedCommands(cli.getCommands())) {
throw new RuntimeException("CommandLineInterpreter interface of " + cli + " is not properly implemented");
}
this.cli = cli;
this.input = input;
this.output = output;
}
private boolean hasPredefinedCommands(Set<? extends Command<?>> commands) {
return !Collections.disjoint(commands, getCommands());
}
public void start() {
Scanner scanner = new Scanner(input);
showHelp();
while (isRunning) {
output.print("$>");
String command = scanner.nextLine();
List<String> line = Arrays.asList(command.split(COMMAND_SEPARATOR));
String identifier = line.get(0);
List<String> parameters = line.subList(1, line.size());
if (isExecutableCommand(identifier)) {
Response response = executeCommand(identifier, parameters);
if (response.failed()) {
showResponse(response);
}
} else {
showHelp();
}
}
}
private boolean isExecutableCommand(String identifier) {
for (Command cmd : getAllCommands()) {
if (cmd.isIdentifier(identifier)) {
return true;
}
}
return false;
}
private void showHelp() {
Set<Command<?>> commands = getAllCommands();
output.println("help - these commands are available:");
commands.forEach(c -> output.printf(" - %s\n", c.getIdentifier()));
}
private void showResponse(Response response) {
output.println(response);
}
@Override
public Set<Command<CommandLineInterface>> getCommands() {
Set<Command<CommandLineInterface>> cliCommands = new HashSet<>();
cliCommands.add(new Command<CommandLineInterface>("exit") {
@Override
public Response execute(CommandLineInterface commandLineInterface, List<String> parameter) {
isRunning = false;
return Response.success();
}
});
cliCommands.add(new Command<CommandLineInterface>("help") {
@Override
public Response execute(CommandLineInterface commandLineInterface, List<String> parameter) {
showHelp();
return Response.success();
}
});
return cliCommands;
}
private Set<Command<?>> getAllCommands() {
Set<Command<?>> commands = mapCommands(cli.getCommands());
commands.addAll(getCommands());
return commands;
}
private Set<Command<?>> mapCommands(Set commands) {
Set<Command<?>> mappedCommands = new HashSet<>();
for (Object o : commands) mapCommand(o).ifPresent(mappedCommands::add);
return mappedCommands;
}
private Optional<Command<?>> mapCommand(Object o) {
return (o instanceof Command<?>) ? Optional.of((Command<?>) o) : Optional.empty();
}
@Override
public Response executeCommand(String identifier, List<String> parameter) {
Optional<Command<CommandLineInterface>> cmd = getCommands().stream().filter(command -> command.isIdentifier(identifier)).findAny();
if (cmd.isPresent()) {
return cmd.get().execute(this, parameter);
} else {
return cli.executeCommand(identifier, parameter);
}
}
}
Example
If you have implemented the interfaces properly you should have easy to use CLI support for your apps:
public static void main(String[] args) {
ExampleApplication app = new ExampleApplication();//implements CommandLineInterpreter
CommandLineInterface commandLineInterface = new CommandLineInterface(app, System.in, System.out);
commandLineInterface.start();
}
Output
this is an example output from an example app that does not much...
help - these commands are available:
- exampleCommand
- help
- exit
$>help
help - these commands are available:
- exampleCommand
- help
- exit
$>exampleCommand p1 p2 p3
i could do some actual work now if i were not a mere example, parameter [p1, p2, p3]
$>exit
Process finished with exit code 0