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