summaryrefslogtreecommitdiff
path: root/base/src/main/java/bjc/utils/cli
diff options
context:
space:
mode:
Diffstat (limited to 'base/src/main/java/bjc/utils/cli')
-rw-r--r--base/src/main/java/bjc/utils/cli/CLICommander.java134
-rw-r--r--base/src/main/java/bjc/utils/cli/Command.java39
-rw-r--r--base/src/main/java/bjc/utils/cli/CommandHandler.java24
-rw-r--r--base/src/main/java/bjc/utils/cli/CommandHelp.java31
-rw-r--r--base/src/main/java/bjc/utils/cli/CommandMode.java72
-rw-r--r--base/src/main/java/bjc/utils/cli/DelegatingCommand.java64
-rw-r--r--base/src/main/java/bjc/utils/cli/GenericCommand.java84
-rw-r--r--base/src/main/java/bjc/utils/cli/GenericCommandMode.java469
-rw-r--r--base/src/main/java/bjc/utils/cli/GenericHelp.java63
-rw-r--r--base/src/main/java/bjc/utils/cli/NullHelp.java20
-rw-r--r--base/src/main/java/bjc/utils/cli/objects/BlockReaderCLI.java392
-rw-r--r--base/src/main/java/bjc/utils/cli/objects/Command.java87
-rw-r--r--base/src/main/java/bjc/utils/cli/objects/DefineCLI.java133
13 files changed, 1612 insertions, 0 deletions
diff --git a/base/src/main/java/bjc/utils/cli/CLICommander.java b/base/src/main/java/bjc/utils/cli/CLICommander.java
new file mode 100644
index 0000000..cccb255
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/CLICommander.java
@@ -0,0 +1,134 @@
+package bjc.utils.cli;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.util.Arrays;
+import java.util.Scanner;
+
+/**
+ * Runs a CLI interface from the provided set of streams.
+ *
+ * @author ben
+ *
+ */
+public class CLICommander {
+ /*
+ * The streams used for input and normal/error output.
+ */
+ private final InputStream input;
+ private final OutputStream output;
+ private final OutputStream error;
+
+ /*
+ * The command mode to start execution in.
+ */
+ private CommandMode initialMode;
+
+ /**
+ * Create a new CLI interface powered by streams.
+ *
+ * @param input
+ * The stream to get user input from.
+ * @param output
+ * The stream to send normal output to.
+ * @param error
+ * The stream to send error output to.
+ */
+ public CLICommander(final InputStream input, final OutputStream output, final OutputStream error) {
+ if (input == null)
+ throw new NullPointerException("Input stream must not be null");
+ else if (output == null)
+ throw new NullPointerException("Output stream must not be null");
+ else if (error == null) throw new NullPointerException("Error stream must not be null");
+
+ this.input = input;
+ this.output = output;
+ this.error = error;
+ }
+
+ /**
+ * Start handling commands from the given input stream.
+ */
+ public void runCommands() {
+ /*
+ * Setup output streams.
+ */
+ final PrintStream normalOutput = new PrintStream(output);
+ final PrintStream errorOutput = new PrintStream(error);
+
+ /*
+ * Set up input streams.
+ *
+ * We're suppressing the warning because we might use the input
+ * stream multiple times.
+ */
+ @SuppressWarnings("resource")
+ final Scanner inputSource = new Scanner(input);
+
+ /*
+ * The mode currently being used to handle commands.
+ *
+ * Used to preserve the initial mode.
+ */
+ CommandMode currentMode = initialMode;
+
+ /*
+ * Process commands until we're told to stop.
+ */
+ while (currentMode != null) {
+ /*
+ * Print out the command prompt, using a custom prompt
+ * if one is specified.
+ */
+ if (currentMode.isCustomPromptEnabled()) {
+ normalOutput.print(currentMode.getCustomPrompt());
+ } else {
+ normalOutput.print(currentMode.getName() + ">> ");
+ }
+
+ /*
+ * Read in a command.
+ */
+ final String currentLine = inputSource.nextLine();
+
+ /*
+ * Handle commands we can handle.
+ */
+ if (currentMode.canHandle(currentLine)) {
+ final String[] commandTokens = currentLine.split(" ");
+ String[] commandArgs = null;
+
+ final int argCount = commandTokens.length;
+
+ /*
+ * Parse args if they are present.
+ */
+ if (argCount > 1) {
+ commandArgs = Arrays.copyOfRange(commandTokens, 1, argCount);
+ }
+
+ /*
+ * Process command.
+ */
+ currentMode = currentMode.process(commandTokens[0], commandArgs);
+ } else {
+ errorOutput.print("Error: Unrecognized command " + currentLine);
+ }
+ }
+
+ normalOutput.print("Exiting now.");
+ }
+
+ /**
+ * Set the initial command mode to use.
+ *
+ * @param initialMode
+ * The initial command mode to use.
+ */
+ public void setInitialCommandMode(final CommandMode initialMode) {
+ if (initialMode == null) throw new NullPointerException("Initial mode must be non-zero");
+
+ this.initialMode = initialMode;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/cli/Command.java b/base/src/main/java/bjc/utils/cli/Command.java
new file mode 100644
index 0000000..02bc061
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/Command.java
@@ -0,0 +1,39 @@
+package bjc.utils.cli;
+
+/**
+ * Represents a command that can be invoked from a {@link CommandMode}
+ *
+ * @author ben
+ *
+ */
+public interface Command {
+ /**
+ * Create a command that serves as an alias to this one
+ *
+ * @return A command that serves as an alias to this one
+ */
+ Command aliased();
+
+ /**
+ * Get the handler that executes this command
+ *
+ * @return The handler that executes this command
+ */
+ CommandHandler getHandler();
+
+ /**
+ * Get the help entry for this command
+ *
+ * @return The help entry for this command
+ */
+ CommandHelp getHelp();
+
+ /**
+ * Check if this command is an alias of another command
+ *
+ * @return Whether or not this command is an alias of another
+ */
+ default boolean isAlias() {
+ return false;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/cli/CommandHandler.java b/base/src/main/java/bjc/utils/cli/CommandHandler.java
new file mode 100644
index 0000000..2548248
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/CommandHandler.java
@@ -0,0 +1,24 @@
+package bjc.utils.cli;
+
+import java.util.function.Function;
+
+/**
+ * A handler for a command
+ *
+ * @author ben
+ *
+ */
+@FunctionalInterface
+public interface CommandHandler extends Function<String[], CommandMode> {
+ /**
+ * Execute this command
+ *
+ * @param args
+ * The arguments for this command
+ * @return The command mode to switch to after this command, or null to
+ * stop executing commands
+ */
+ default CommandMode handle(final String[] args) {
+ return this.apply(args);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/cli/CommandHelp.java b/base/src/main/java/bjc/utils/cli/CommandHelp.java
new file mode 100644
index 0000000..86567a0
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/CommandHelp.java
@@ -0,0 +1,31 @@
+package bjc.utils.cli;
+
+/**
+ * Interface for the help entry for a command
+ *
+ * @author ben
+ *
+ */
+public interface CommandHelp {
+ /**
+ * Get the description of a command.
+ *
+ * @return The description of a command
+ */
+ String getDescription();
+
+ /**
+ * Get the summary line for a command.
+ *
+ * A summary line should consist of a string of the following format
+ *
+ * <pre>
+ * "&lt;command-name>\t&lt;command-summary>"
+ * </pre>
+ *
+ * where anything in angle brackets should be filled in.
+ *
+ * @return The summary line line for a command
+ */
+ String getSummary();
+}
diff --git a/base/src/main/java/bjc/utils/cli/CommandMode.java b/base/src/main/java/bjc/utils/cli/CommandMode.java
new file mode 100644
index 0000000..39c72fc
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/CommandMode.java
@@ -0,0 +1,72 @@
+package bjc.utils.cli;
+
+/**
+ * A mode for determining the commands that are valid to enter, and then
+ * handling those commands
+ *
+ * @author ben
+ *
+ */
+public interface CommandMode extends Comparable<CommandMode> {
+ /**
+ * Check to see if this mode can handle the specified command
+ *
+ * @param command
+ * The command to check
+ * @return Whether or not this mode can handle the command. It is
+ * assumed not by default
+ */
+ default boolean canHandle(final String command) {
+ return false;
+ };
+
+ /**
+ * Get the custom prompt for this mode
+ *
+ * @return the custom prompt for this mode
+ *
+ * @throws UnsupportedOperationException
+ * if this mode doesn't support a custom prompt
+ */
+ default String getCustomPrompt() {
+ throw new UnsupportedOperationException("This mode doesn't support a custom prompt");
+ }
+
+ /**
+ * Get the name of this command mode
+ *
+ * @return The name of this command mode, which is the empty string by
+ * default
+ */
+ public default String getName() {
+ return "";
+ }
+
+ /**
+ * Check if this mode uses a custom prompt
+ *
+ * @return Whether or not this mode uses a custom prompt
+ */
+ default boolean isCustomPromptEnabled() {
+ return false;
+ }
+
+ /**
+ * Process a command in this mode
+ *
+ * @param command
+ * The command to process
+ * @param args
+ * A list of arguments to the command
+ * @return The command mode to use for the next command. Defaults to
+ * returning this, and doing nothing else
+ */
+ default CommandMode process(final String command, final String[] args) {
+ return this;
+ }
+
+ @Override
+ default int compareTo(final CommandMode o) {
+ return getName().compareTo(o.getName());
+ }
+}
diff --git a/base/src/main/java/bjc/utils/cli/DelegatingCommand.java b/base/src/main/java/bjc/utils/cli/DelegatingCommand.java
new file mode 100644
index 0000000..acaa3a6
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/DelegatingCommand.java
@@ -0,0 +1,64 @@
+package bjc.utils.cli;
+
+/**
+ * A class for a command that delegates to another command.
+ *
+ * @author ben
+ *
+ */
+class DelegatingCommand implements Command {
+ /*
+ * The command to delegate to.
+ */
+ private final Command delegate;
+
+ /**
+ * Create a new command that delegates to another command.
+ *
+ * @param delegate
+ * The command to delegate to.
+ */
+ public DelegatingCommand(final Command delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Command aliased() {
+ return new DelegatingCommand(delegate);
+ }
+
+ @Override
+ public CommandHandler getHandler() {
+ return delegate.getHandler();
+ }
+
+ @Override
+ public CommandHelp getHelp() {
+ return delegate.getHelp();
+ }
+
+ @Override
+ public boolean isAlias() {
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("DelegatingCommand [");
+
+ if (delegate != null) {
+ builder.append("delegate=");
+ builder.append(delegate);
+ }
+
+ builder.append("]");
+
+ return builder.toString();
+ }
+}
diff --git a/base/src/main/java/bjc/utils/cli/GenericCommand.java b/base/src/main/java/bjc/utils/cli/GenericCommand.java
new file mode 100644
index 0000000..4ae4dea
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/GenericCommand.java
@@ -0,0 +1,84 @@
+package bjc.utils.cli;
+
+/**
+ * Generic command implementation.
+ *
+ * @author ben
+ *
+ */
+public class GenericCommand implements Command {
+ /*
+ * The behavior for invoking the command.
+ */
+ private final CommandHandler handler;
+
+ /*
+ * The help for the command.
+ */
+ private CommandHelp help;
+
+ /**
+ * Create a new generic command.
+ *
+ * @param handler
+ * The handler to use for the command.
+ * @param description
+ * The description of the command. May be null, in which
+ * case a default is provided.
+ * @param help
+ * The detailed help message for the command. May be
+ * null, in which case the description is repeated for
+ * the detailed help.
+ */
+ public GenericCommand(final CommandHandler handler, final String description, final String help) {
+ if (handler == null) throw new NullPointerException("Command handler must not be null");
+
+ this.handler = handler;
+
+ if (description == null) {
+ this.help = new NullHelp();
+ } else {
+ this.help = new GenericHelp(description, help);
+ }
+ }
+
+ @Override
+ public Command aliased() {
+ return new DelegatingCommand(this);
+ }
+
+ @Override
+ public CommandHandler getHandler() {
+ return handler;
+ }
+
+ @Override
+ public CommandHelp getHelp() {
+ return help;
+ }
+
+ @Override
+ public boolean isAlias() {
+ return false;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("GenericCommand [");
+
+ if (help != null) {
+ builder.append("help=");
+ builder.append(help);
+ }
+
+ builder.append("]");
+
+ return builder.toString();
+ }
+}
diff --git a/base/src/main/java/bjc/utils/cli/GenericCommandMode.java b/base/src/main/java/bjc/utils/cli/GenericCommandMode.java
new file mode 100644
index 0000000..8764537
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/GenericCommandMode.java
@@ -0,0 +1,469 @@
+package bjc.utils.cli;
+
+import java.util.TreeMap;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+import bjc.utils.funcdata.FunctionalMap;
+import bjc.utils.funcdata.IMap;
+
+/**
+ * A general command mode, with a customizable set of commands
+ *
+ * There is a small set of commands which is handled by default. The first is
+ * 'list', which lists all the commands the user can input. The second is
+ * 'alias', which allows the user to bind a new name to a command
+ *
+ * @author ben
+ *
+ */
+public class GenericCommandMode implements CommandMode {
+ /*
+ * Contains the commands this mode handles
+ */
+ private final IMap<String, Command> commandHandlers;
+ private final IMap<String, Command> defaultHandlers;
+
+ /*
+ * Contains help topics without an associated command
+ */
+ private final IMap<String, CommandHelp> helpTopics;
+
+ /*
+ * The action to execute upon encountering an unknown command
+ */
+ private BiConsumer<String, String[]> unknownCommandHandler;
+
+ /*
+ * The functions to use for input/output
+ */
+ private final Consumer<String> errorOutput;
+ private final Consumer<String> normalOutput;
+
+ /*
+ * The name of this command mode, or null if it is unnamed
+ */
+ private String modeName;
+
+ /*
+ * The custom prompt to use, or null if none is specified
+ */
+ private String customPrompt;
+
+ /**
+ * Create a new generic command mode
+ *
+ * @param normalOutput
+ * The function to use for normal output
+ * @param errorOutput
+ * The function to use for error output
+ */
+ public GenericCommandMode(final Consumer<String> normalOutput, final Consumer<String> errorOutput) {
+ if (normalOutput == null)
+ throw new NullPointerException("Normal output source must be non-null");
+ else if (errorOutput == null) throw new NullPointerException("Error output source must be non-null");
+
+ this.normalOutput = normalOutput;
+ this.errorOutput = errorOutput;
+
+ /*
+ * Initialize handler maps so that they sort in alphabetical
+ */
+ /*
+ * order
+ */
+ commandHandlers = new FunctionalMap<>(new TreeMap<>());
+ defaultHandlers = new FunctionalMap<>(new TreeMap<>());
+ helpTopics = new FunctionalMap<>(new TreeMap<>());
+
+ setupDefaultCommands();
+ }
+
+ /**
+ * Add an alias to an existing command
+ *
+ * @param commandName
+ * The name of the command to add an alias for
+ * @param aliasName
+ * The new alias for the command
+ *
+ * @throws IllegalArgumentException
+ * if the specified command doesn't have a bound
+ * handler, or if the alias name already has a bound
+ * value
+ */
+ public void addCommandAlias(final String commandName, final String aliasName) {
+ if (commandName == null)
+ throw new NullPointerException("Command name must not be null");
+ else if (aliasName == null)
+ throw new NullPointerException("Alias name must not be null");
+ else if (!commandHandlers.containsKey(commandName) && !defaultHandlers.containsKey(commandName))
+ throw new IllegalArgumentException("Cannot alias non-existant command '" + commandName + "'");
+ else if (commandHandlers.containsKey(aliasName) || defaultHandlers.containsKey(aliasName))
+ throw new IllegalArgumentException(
+ "Cannot bind alias '" + aliasName + "' to a command with a bound handler");
+ else {
+ Command aliasedCommand;
+
+ if (defaultHandlers.containsKey(commandName)) {
+ aliasedCommand = defaultHandlers.get(commandName).aliased();
+ } else {
+ aliasedCommand = commandHandlers.get(commandName).aliased();
+ }
+
+ commandHandlers.put(aliasName, aliasedCommand);
+ }
+ }
+
+ /**
+ * Add a command to this command mode
+ *
+ * @param command
+ * The name of the command to add
+ * @param handler
+ * The handler to use for the specified command
+ *
+ * @throws IllegalArgumentException
+ * if the specified command already has a handler
+ * registered
+ */
+ public void addCommandHandler(final String command, final Command handler) {
+ if (command == null)
+ throw new NullPointerException("Command must not be null");
+ else if (handler == null)
+ throw new NullPointerException("Handler must not be null");
+ else if (canHandle(command))
+ throw new IllegalArgumentException("Command " + command + " already has a handler registered");
+ else {
+ commandHandlers.put(command, handler);
+ }
+ }
+
+ /**
+ * Add a help topic to this command mode that isn't tied to a command
+ *
+ * @param topicName
+ * The name of the topic
+ * @param topic
+ * The contents of the topic
+ */
+ public void addHelpTopic(final String topicName, final CommandHelp topic) {
+ helpTopics.put(topicName, topic);
+ }
+
+ /*
+ * Default command builders
+ */
+
+ private GenericCommand buildAliasCommand() {
+ final String aliasShortHelp = "alias\tAlias one command to another";
+ final String aliasLongHelp = "Gives a command another name it can be invoked by."
+ + " Invoke with two arguments: the name of the command to alias"
+ + "followed by the name of the alias to give that command.";
+
+ return new GenericCommand((args) -> {
+ doAliasCommands(args);
+
+ return this;
+ }, aliasShortHelp, aliasLongHelp);
+ }
+
+ private GenericCommand buildClearCommands() {
+ final String clearShortHelp = "clear\tClear the screen";
+ final String clearLongHelp = "Clears the screen of all the text on it," + " and prints a new prompt.";
+
+ return new GenericCommand((args) -> {
+ errorOutput.accept("ERROR: This console doesn't support screen clearing");
+
+ return this;
+ }, clearShortHelp, clearLongHelp);
+ }
+
+ private GenericCommand buildExitCommand() {
+ final String exitShortHelp = "exit\tExit the console";
+ final String exitLongHelp = "First prompts the user to make sure they want to"
+ + " exit, then quits if they say they do";
+
+ return new GenericCommand((args) -> {
+ errorOutput.accept("ERROR: This console doesn't support auto-exiting");
+
+ return this;
+ }, exitShortHelp, exitLongHelp);
+ }
+
+ private GenericCommand buildHelpCommand() {
+ final String helpShortHelp = "help\tConsult the help system";
+ final String helpLongHelp = "Consults the internal help system."
+ + " Invoked in two different ways. Invoking with no arguments"
+ + " causes all the topics you can ask for details on to be list,"
+ + " while invoking with the name of a topic will print the entry" + " for that topic";
+
+ return new GenericCommand((args) -> {
+ if (args == null || args.length == 0) {
+ /*
+ * Invoke general help
+ */
+ doHelpSummary();
+ } else {
+ /*
+ * Invoke help for a command
+ */
+ doHelpCommand(args[0]);
+ }
+
+ return this;
+ }, helpShortHelp, helpLongHelp);
+ }
+
+ private GenericCommand buildListCommand() {
+ final String listShortHelp = "list\tList available commands";
+ final String listLongHelp = "Lists all of the commands available in this mode,"
+ + " as well as commands available in any mode";
+
+ return new GenericCommand((args) -> {
+ doListCommands();
+
+ return this;
+ }, listShortHelp, listLongHelp);
+ }
+
+ @Override
+ public boolean canHandle(final String command) {
+ return commandHandlers.containsKey(command) || defaultHandlers.containsKey(command);
+ }
+
+ /*
+ * Implement default commands
+ */
+
+ private void doAliasCommands(final String[] args) {
+ if (args.length != 2) {
+ errorOutput.accept("ERROR: Alias requires two arguments."
+ + " The command name, and the alias for that command");
+ } else {
+ final String commandName = args[0];
+ final String aliasName = args[1];
+
+ if (!canHandle(commandName)) {
+ errorOutput.accept("ERROR: '" + commandName + "' is not a valid command.");
+ } else if (canHandle(aliasName)) {
+ errorOutput.accept("ERROR: Cannot overwrite command '" + aliasName + "'");
+ } else {
+ addCommandAlias(commandName, aliasName);
+ }
+ }
+ }
+
+ private void doHelpCommand(final String commandName) {
+ if (commandHandlers.containsKey(commandName)) {
+ final String desc = commandHandlers.get(commandName).getHelp().getDescription();
+
+ normalOutput.accept("\n" + desc);
+ } else if (defaultHandlers.containsKey(commandName)) {
+ final String desc = defaultHandlers.get(commandName).getHelp().getDescription();
+
+ normalOutput.accept("\n" + desc);
+ } else if (helpTopics.containsKey(commandName)) {
+ normalOutput.accept("\n" + helpTopics.get(commandName).getDescription());
+ } else {
+ errorOutput.accept(
+ "ERROR: I'm sorry, but there is no help available for '" + commandName + "'");
+ }
+ }
+
+ private void doHelpSummary() {
+ normalOutput.accept("Help topics for this command mode are as follows:\n");
+
+ if (commandHandlers.size() > 0) {
+ commandHandlers.forEachValue(command -> {
+ if (!command.isAlias()) {
+ normalOutput.accept("\t" + command.getHelp().getSummary() + "\n");
+ }
+ });
+ } else {
+ normalOutput.accept("\tNone available\n");
+ }
+
+ normalOutput.accept("\nHelp topics available in all command modes are as follows\n");
+ if (defaultHandlers.size() > 0) {
+ defaultHandlers.forEachValue(command -> {
+ if (!command.isAlias()) {
+ normalOutput.accept("\t" + command.getHelp().getSummary() + "\n");
+ }
+ });
+ } else {
+ normalOutput.accept("\tNone available\n");
+ }
+
+ normalOutput.accept("\nHelp topics not associated with a command are as follows\n");
+ if (helpTopics.size() > 0) {
+ helpTopics.forEachValue(topic -> {
+ normalOutput.accept("\t" + topic.getSummary() + "\n");
+ });
+ } else {
+ normalOutput.accept("\tNone available\n");
+ }
+ }
+
+ private void doListCommands() {
+ normalOutput.accept("The available commands for this mode are as follows:\n");
+
+ commandHandlers.keyList().forEach(commandName -> {
+ normalOutput.accept("\t" + commandName);
+ });
+
+ normalOutput.accept("\nThe following commands are available in all modes:\n");
+ defaultHandlers.keyList().forEach(commandName -> {
+ normalOutput.accept("\t" + commandName);
+ });
+
+ normalOutput.accept("\n");
+ }
+
+ @Override
+ public String getCustomPrompt() {
+ if (customPrompt != null) return customPrompt;
+
+ return CommandMode.super.getCustomPrompt();
+ }
+
+ @Override
+ public String getName() {
+ if (modeName != null) return modeName;
+
+ return CommandMode.super.getName();
+ }
+
+ @Override
+ public boolean isCustomPromptEnabled() {
+ return customPrompt != null;
+ }
+
+ @Override
+ public CommandMode process(final String command, final String[] args) {
+ normalOutput.accept("\n");
+
+ if (defaultHandlers.containsKey(command))
+ return defaultHandlers.get(command).getHandler().handle(args);
+ else if (commandHandlers.containsKey(command))
+ return commandHandlers.get(command).getHandler().handle(args);
+ else {
+ if (args != null) {
+ errorOutput.accept("ERROR: Unrecognized command " + command + String.join(" ", args));
+ } else {
+ errorOutput.accept("ERROR: Unrecognized command " + command);
+ }
+
+ if (unknownCommandHandler == null)
+ throw new UnsupportedOperationException("Command " + command + " is invalid.");
+
+ unknownCommandHandler.accept(command, args);
+ }
+
+ return this;
+ }
+
+ /**
+ * Set the custom prompt for this mode
+ *
+ * @param prompt
+ * The custom prompt for this mode, or null to disable
+ * the custom prompt
+ */
+ public void setCustomPrompt(final String prompt) {
+ customPrompt = prompt;
+ }
+
+ /**
+ * Set the name of this mode
+ *
+ * @param name
+ * The desired name of this mode, or null to use the
+ * default name
+ */
+ public void setModeName(final String name) {
+ modeName = name;
+ }
+
+ /**
+ * Set the handler to use for unknown commands
+ *
+ * @param handler
+ * The handler to use for unknown commands, or null to
+ * throw on unknown commands
+ */
+ public void setUnknownCommandHandler(final BiConsumer<String, String[]> handler) {
+ if (handler == null) throw new NullPointerException("Handler must not be null");
+
+ unknownCommandHandler = handler;
+ }
+
+ private void setupDefaultCommands() {
+ defaultHandlers.put("list", buildListCommand());
+ defaultHandlers.put("alias", buildAliasCommand());
+ defaultHandlers.put("help", buildHelpCommand());
+
+ addCommandAlias("help", "man");
+
+ /*
+ * Add commands handled in a upper layer.
+ */
+
+ /*
+ * @TODO figure out a place to put commands that apply across
+ */
+ /*
+ * all
+ */
+ /*
+ * modes, but only apply to a specific application
+ */
+ defaultHandlers.put("clear", buildClearCommands());
+ defaultHandlers.put("exit", buildExitCommand());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("GenericCommandMode [");
+
+ if (commandHandlers != null) {
+ builder.append("commandHandlers=");
+ builder.append(commandHandlers);
+ }
+
+ if (defaultHandlers != null) {
+ builder.append(", ");
+ builder.append("defaultHandlers=");
+ builder.append(defaultHandlers);
+ }
+
+ if (helpTopics != null) {
+ builder.append(", ");
+ builder.append("helpTopics=");
+ builder.append(helpTopics);
+ }
+
+ if (modeName != null) {
+ builder.append(", ");
+ builder.append("modeName=");
+ builder.append(modeName);
+ }
+
+ if (customPrompt != null) {
+ builder.append(", ");
+ builder.append("customPrompt=");
+ builder.append(customPrompt);
+ }
+
+ builder.append("]");
+
+ return builder.toString();
+ }
+
+}
diff --git a/base/src/main/java/bjc/utils/cli/GenericHelp.java b/base/src/main/java/bjc/utils/cli/GenericHelp.java
new file mode 100644
index 0000000..38adf57
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/GenericHelp.java
@@ -0,0 +1,63 @@
+package bjc.utils.cli;
+
+/**
+ * Generic implementation of a help topic
+ *
+ * @author ben
+ *
+ */
+public class GenericHelp implements CommandHelp {
+ // The strings for this help topic
+ private final String summary;
+ private final String description;
+
+ /**
+ * Create a new help topic
+ *
+ * @param summary
+ * The summary of this help topic
+ * @param description
+ * The description of this help topic, or null if this
+ * help topic doesn't have a more detailed description
+ */
+ public GenericHelp(final String summary, final String description) {
+ if (summary == null) throw new NullPointerException("Help summary must be non-null");
+
+ this.summary = summary;
+ this.description = description;
+ }
+
+ @Override
+ public String getDescription() {
+ if (description == null) return summary;
+
+ return description;
+ }
+
+ @Override
+ public String getSummary() {
+ return summary;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+
+ builder.append("GenericHelp [");
+
+ if (summary != null) {
+ builder.append("summary=");
+ builder.append(summary);
+ }
+
+ if (description != null) {
+ builder.append(", ");
+ builder.append("description=");
+ builder.append(description);
+ }
+
+ builder.append("]");
+
+ return builder.toString();
+ }
+}
diff --git a/base/src/main/java/bjc/utils/cli/NullHelp.java b/base/src/main/java/bjc/utils/cli/NullHelp.java
new file mode 100644
index 0000000..6c49ae6
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/NullHelp.java
@@ -0,0 +1,20 @@
+package bjc.utils.cli;
+
+/**
+ * Implementation of a help topic that doesn't exist
+ *
+ * @author ben
+ *
+ */
+public class NullHelp implements CommandHelp {
+ @Override
+ public String getDescription() {
+ return "No description provided";
+ }
+
+ @Override
+ public String getSummary() {
+ return "No summary provided";
+ }
+
+}
diff --git a/base/src/main/java/bjc/utils/cli/objects/BlockReaderCLI.java b/base/src/main/java/bjc/utils/cli/objects/BlockReaderCLI.java
new file mode 100644
index 0000000..ec66fe2
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/objects/BlockReaderCLI.java
@@ -0,0 +1,392 @@
+package bjc.utils.cli.objects;
+
+import java.io.InputStreamReader;
+import java.io.Reader;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.function.Predicate;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import bjc.utils.ioutils.Prompter;
+import bjc.utils.ioutils.blocks.*;
+
+import static bjc.utils.cli.objects.Command.CommandStatus;
+import static bjc.utils.cli.objects.Command.CommandStatus.*;
+
+public class BlockReaderCLI {
+ private final Logger LOGGER = Logger.getLogger(BlockReaderCLI.class.getName());
+
+ public static class BlockReaderState {
+ public final Map<String, BlockReader> readers;
+ public final Map<String, Reader> sources;
+
+ public BlockReaderState(Map<String, BlockReader> readers, Map<String, Reader> sources) {
+ this.readers = readers;
+ this.sources = sources;
+ }
+ }
+
+ private BlockReaderState stat;
+
+ /**
+ * Create a new CLI for configuring BlockReaders.
+ *
+ * @param srcs
+ * The container of initial I/O sources.
+ */
+ public BlockReaderCLI(Map<String, Reader> srcs) {
+ stat = new BlockReaderState(new HashMap<>(), srcs);
+ }
+
+ public static void main(String[] args) {
+ /*
+ * Create/configure I/O sources.
+ */
+ Map<String, Reader> sources = new HashMap<>();
+ sources.put("stdio", new InputStreamReader(System.in));
+
+ BlockReaderCLI reader = new BlockReaderCLI(sources);
+
+ reader.run(new Scanner(System.in), "console", true);
+ }
+
+ /**
+ * Run the CLI on an input source.
+ *
+ * @param input
+ * The place to read input from.
+ * @param ioSource
+ * The name of the place to read input from.
+ * @param interactive
+ * Whether or not the source is interactive
+ */
+ public void run(Scanner input, String ioSource, boolean interactive) {
+ int lno = 0;
+ while(input.hasNextLine()) {
+ if(interactive)
+ System.out.printf("reader-conf(%d)>", lno);
+
+ String ln = input.nextLine();
+
+ lno += 1;
+
+ Command com = Command.fromString(ln, lno, ioSource);
+ if(com == null) continue;
+
+ CommandStatus stat = handleCommand(com, interactive);
+ if(stat == FINISH || stat == ERROR) {
+ return;
+ }
+ }
+
+ input.close();
+ }
+
+ /*
+ * Handle a command.
+ */
+ public CommandStatus handleCommand(Command com, boolean interactive) {
+ switch(com.nameCommand) {
+ case "def-filtered":
+ return defFiltered(com);
+ case "def-layered":
+ return defLayered(com);
+ case "def-pushback":
+ return defPushback(com);
+ case "def-simple":
+ return defSimple(com);
+ case "def-serial":
+ return defSerial(com);
+ case "def-toggled":
+ return defToggled(com);
+ case "}":
+ case "end":
+ case "exit":
+ case "quit":
+ if(interactive)
+ System.out.printf("Exiting reader-conf, %d readers configured in %d commands\n",
+ stat.readers.size(), com.lineNo);
+ return FINISH;
+ default:
+ LOGGER.severe(com.error("Unknown command '%s'\n", com.nameCommand));
+ return FAIL;
+ }
+ }
+
+ private CommandStatus defFiltered(Command com) {
+ String remn = com.remnCommand;
+
+ /*
+ * Get the block name.
+ */
+ int idx = remn.indexOf(' ');
+ if(idx == -1) {
+ LOGGER.severe(com.error("No name argument for def-filtered.\n"));
+ return FAIL;
+ }
+ String blockName = remn.substring(0, idx).trim();
+ remn = remn.substring(idx).trim();
+
+ /*
+ * Check there isn't a reader already bound to this name.
+ */
+ if(stat.readers.containsKey(blockName)) {
+ LOGGER.warning(com.warn("Shadowing existing reader named %s\n", blockName));
+ }
+
+ /*
+ * Get the reader name.
+ */
+ idx = remn.indexOf(' ');
+ if(idx == -1) {
+ LOGGER.severe(com.error("No reader-name argument for def-filtered.\n"));
+ return FAIL;
+ }
+ String readerName = remn.substring(0, idx).trim();
+ remn = remn.substring(idx).trim();
+
+ /*
+ * Check there is a reader bound to that name.
+ */
+ if(!stat.readers.containsKey(readerName)) {
+ LOGGER.severe(com.error("No source named %s\n", readerName));
+ return FAIL;
+ }
+
+ /*
+ * Get the pattern.
+ */
+ if(remn.equals("")) {
+ LOGGER.severe(com.error("No filter argument for def-filtered\n"));
+ return FAIL;
+ }
+
+ String filter = remn;
+
+ try {
+ Pattern pat = Pattern.compile(filter);
+
+ Predicate<Block> pred = (block) -> {
+ Matcher mat = pat.matcher(block.contents);
+
+ return mat.matches();
+ };
+
+ BlockReader reader = new FilteredBlockReader(stat.readers.get(readerName), pred);
+
+ stat.readers.put(blockName, reader);
+ } catch (PatternSyntaxException psex) {
+ LOGGER.severe(com.error("Invalid regular expression '%s' for filter. (%s)\n", filter, psex.getMessage()));
+ return FAIL;
+ }
+
+ return SUCCESS;
+ }
+
+ private CommandStatus defPushback(Command com) {
+ String[] parts = com.remnCommand.split(" ");
+
+ if(parts.length != 2) {
+ LOGGER.severe(com.error("Incorrect number of arguments to def-pushback. Requires a block name and a reader name\n"));
+ return FAIL;
+ }
+
+ String blockName = parts[0];
+ if(stat.readers.containsKey(blockName)) {
+ LOGGER.warning(com.warn("Shadowing existing reader %s\n", blockName));
+ return FAIL;
+ }
+
+ String readerName = parts[1];
+ if(!stat.readers.containsKey(readerName)) {
+ LOGGER.severe(com.error("No reader named %s\n", readerName));
+ return FAIL;
+ }
+
+ BlockReader reader = new PushbackBlockReader(stat.readers.get(readerName));
+ stat.readers.put(blockName, reader);
+
+ return SUCCESS;
+ }
+
+ private CommandStatus defToggled(Command com) {
+ String[] parts = com.remnCommand.split(" ");
+
+ if(parts.length != 3) {
+ LOGGER.severe(com.error("Incorrect number of arguments to def-toggled. Requires a block name and two reader names\n"));
+ return FAIL;
+ }
+
+ /*
+ * Get the block name.
+ */
+ String blockName = parts[0];
+ if(stat.readers.containsKey(blockName)) {
+ LOGGER.warning(com.warn("Shadowing existing reader named %s\n", blockName));
+ }
+
+ /*
+ * Make sure the component readers exist.
+ */
+ if(!stat.readers.containsKey(parts[1])) {
+ LOGGER.severe(com.error("No reader named %s\n", parts[1]));
+ return FAIL;
+ }
+
+ if(!stat.readers.containsKey(parts[2])) {
+ LOGGER.severe(com.error("No reader named %s\n", parts[2]));
+ return FAIL;
+ }
+
+ BlockReader reader = new ToggledBlockReader(stat.readers.get(parts[1]), stat.readers.get(parts[2]));
+ stat.readers.put(blockName, reader);
+
+ return SUCCESS;
+ }
+
+ private CommandStatus defLayered(Command com) {
+ String[] parts = com.remnCommand.split(" ");
+
+ if(parts.length != 3) {
+ LOGGER.severe(com.error("Incorrect number of arguments to def-layered. Requires a block name and two reader names\n"));
+ return FAIL;
+ }
+
+ /*
+ * Get the block name.
+ */
+ String blockName = parts[0];
+ if(stat.readers.containsKey(blockName)) {
+ LOGGER.warning(com.warn("Shadowing existing reader named %s\n", blockName));
+ }
+
+ /*
+ * Make sure the component readers exist.
+ */
+ if(!stat.readers.containsKey(parts[1])) {
+ LOGGER.severe(com.error("No reader named %s\n", parts[1]));
+ return FAIL;
+ }
+
+ if(!stat.readers.containsKey(parts[2])) {
+ LOGGER.severe(com.error("No reader named %s\n", parts[2]));
+ return FAIL;
+ }
+
+ BlockReader reader = new LayeredBlockReader(stat.readers.get(parts[1]), stat.readers.get(parts[2]));
+ stat.readers.put(blockName, reader);
+
+ return SUCCESS;
+ }
+
+ private CommandStatus defSerial(Command com) {
+ String[] parts = com.remnCommand.split(" ");
+
+ if(parts.length < 2) {
+ LOGGER.severe(com.error("Not enough arguments to def-serial. Requires at least a block name and at least one reader name\n"));
+ return FAIL;
+ }
+
+ /*
+ * Get the name for this BlockReader.
+ */
+ String blockName = parts[0];
+ /*
+ * Check there isn't a reader already bound to this name.
+ */
+ if(stat.readers.containsKey(blockName)) {
+ LOGGER.warning(com.warn("Shadowing existing reader named %s\n", blockName));
+ }
+
+ /*
+ * Get all of the component readers.
+ */
+ BlockReader[] readerArr = new BlockReader[parts.length - 1];
+ for(int i = 1; i < parts.length; i++) {
+ String readerName = parts[i];
+
+ /*
+ * Check there is a reader bound to that name.
+ */
+ if(!stat.readers.containsKey(readerName)) {
+ LOGGER.severe(com.error("No reader named %s\n", readerName));
+ return FAIL;
+ }
+
+ readerArr[i] = stat.readers.get(readerName);
+ }
+
+ BlockReader reader = new SerialBlockReader(readerArr);
+
+ stat.readers.put(blockName, reader);
+
+ return SUCCESS;
+ }
+
+ private CommandStatus defSimple(Command com) {
+ String remn = com.remnCommand;
+
+ /*
+ * Get the block name.
+ */
+ int idx = remn.indexOf(' ');
+ if(idx == -1) {
+ LOGGER.severe(com.error("No name argument for def-simple.\n"));
+ return FAIL;
+ }
+ String blockName = remn.substring(0, idx).trim();
+ remn = remn.substring(idx).trim();
+
+ /*
+ * Check there isn't a reader already bound to this name.
+ */
+ if(stat.readers.containsKey(blockName)) {
+ LOGGER.warning(com.warn("Shadowing existing reader named %s\n", blockName));
+ }
+
+ /*
+ * Get the source name.
+ */
+ idx = remn.indexOf(' ');
+ if(idx == -1) {
+ LOGGER.severe(com.error("No source-name argument for def-simple.\n"));
+ return FAIL;
+ }
+ String sourceName = remn.substring(0, idx).trim();
+ remn = remn.substring(idx).trim();
+
+ /*
+ * Check there is a source bound to that name.
+ */
+ if(!stat.sources.containsKey(sourceName)) {
+ LOGGER.severe(com.error("No source named %s\n", sourceName));
+ return FAIL;
+ }
+
+ /*
+ * Get the pattern.
+ */
+ if(remn.equals("")) {
+ LOGGER.severe(com.error("No delimiter argument for def-simple\n"));
+ return FAIL;
+ }
+
+ String delim = remn;
+
+ try {
+ BlockReader reader = new SimpleBlockReader(delim, stat.sources.get(sourceName));
+
+ stat.readers.put(blockName, reader);
+ } catch (PatternSyntaxException psex) {
+ LOGGER.severe(com.error("Invalid regular expression '%s' for delimiter. (%s)\n", delim, psex.getMessage()));
+ return FAIL;
+ }
+
+ return SUCCESS;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/cli/objects/Command.java b/base/src/main/java/bjc/utils/cli/objects/Command.java
new file mode 100644
index 0000000..e605a2b
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/objects/Command.java
@@ -0,0 +1,87 @@
+package bjc.utils.cli.objects;
+
+public class Command {
+ /**
+ * Command status values.
+ */
+ public static enum CommandStatus {
+ /**
+ * The command succeded.
+ */
+ SUCCESS,
+ /**
+ * The command failed non-fatally.
+ */
+ FAIL,
+ /**
+ * The command failed fatally.
+ */
+ ERROR,
+ /**
+ * The command was the last one.
+ */
+ FINISH,
+ }
+
+ public final int lineNo;
+
+ public final String fullCommand;
+ public final String remnCommand;
+ public final String nameCommand;
+
+ public final String ioSource;
+
+ /**
+ * Create a new command.
+ *
+ * @param ln
+ * The line to get the command from.
+ * @param lno
+ * The number of the line the command came from.
+ * @param ioSrc
+ * The name of where the I/O came from.
+ */
+ public Command(String ln, int lno, String ioSrc) {
+ int idx = ln.indexOf(' ');
+
+ if(idx == -1) idx = ln.length();
+
+ fullCommand = ln;
+ nameCommand = ln.substring(0, idx).trim();
+ remnCommand = ln.substring(idx).trim();
+
+ lineNo = lno;
+
+ ioSource = ioSrc;
+ }
+
+ public static Command fromString(String ln, int lno, String ioSource) {
+ /*
+ * Ignore blank lines and comments.
+ */
+ if(ln.equals("")) return null;
+ if(ln.startsWith("#")) return null;
+
+ /*
+ * Trim off comments part-way through the line.
+ */
+ int idxHash = ln.indexOf('#');
+ if(idxHash != -1) {
+ ln = ln.substring(0, idxHash).trim();
+ }
+
+ return new Command(ln, lno, ioSource);
+ }
+
+ public String warn(String warning, Object... parms) {
+ String msg = String.format(warning, parms);
+
+ return String.format("WARNING (%s:%d): %s", ioSource, lineNo, msg);
+ }
+
+ public String error(String err, Object... parms) {
+ String msg = String.format(err, parms);
+
+ return String.format("ERROR (%s:%d): %s", ioSource, lineNo, msg);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/cli/objects/DefineCLI.java b/base/src/main/java/bjc/utils/cli/objects/DefineCLI.java
new file mode 100644
index 0000000..bb2733f
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/objects/DefineCLI.java
@@ -0,0 +1,133 @@
+package bjc.utils.cli.objects;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.function.UnaryOperator;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+import static bjc.utils.cli.objects.Command.CommandStatus;
+import static bjc.utils.cli.objects.Command.CommandStatus.*;
+
+public class DefineCLI {
+ private final Logger LOGGER = Logger.getLogger(DefineCLI.class.getName());
+
+ public static class DefineState {
+ public final Map<String, UnaryOperator<String>> defines;
+
+ public final Map<String, String> strings;
+ public final Map<String, String> formats;
+
+ public final Map<String, Pattern> patterns;
+
+ public DefineState() {
+ this(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
+ }
+
+ public DefineState(Map<String, UnaryOperator<String>> defines,
+ Map<String, String> strings, Map<String, String> formats,
+ Map<String, Pattern> patterns) {
+ this.defines = defines;
+
+ this.strings = strings;
+ this.formats = formats;
+
+ this.patterns = patterns;
+ }
+ }
+
+ private DefineState stat;
+
+ public DefineCLI() {
+ stat = new DefineState();
+ }
+
+ public static void main(String[] args) {
+ DefineCLI defin = new DefineCLI();
+ }
+
+ /**
+ * Run the CLI on an input source.
+ *
+ * @param input
+ * The place to read input from.
+ * @param ioSource
+ * The name of the place to read input from.
+ * @param interactive
+ * Whether or not the source is interactive
+ */
+ public void run(Scanner input, String ioSource, boolean interactive) {
+ int lno = 0;
+ while(input.hasNextLine()) {
+ if(interactive)
+ System.out.printf("define-conf(%d)>", lno);
+
+ String ln = input.nextLine();
+
+ lno += 1;
+
+ Command com = Command.fromString(ln, lno, ioSource);
+ if(com == null) continue;
+
+ handleCommand(com, interactive);
+ }
+
+ input.close();
+ }
+
+ public void handleCommand(Command com, boolean interactive) {
+ switch(com.nameCommand) {
+ case "def-string":
+ default:
+ LOGGER.severe(com.error("Unknown command %s\n", com.nameCommand));
+ break;
+ }
+ }
+
+ private CommandStatus defString(Command com) {
+ String remn = com.remnCommand;
+
+ int idx = remn.indexOf(' ');
+ if(idx == -1) {
+ LOGGER.warning(com.warn("Binding empty string to name '%s'\n", remn));
+ idx = remn.length();
+ }
+ String name = remn.substring(0, idx);
+ String strang = remn.substring(idx);
+
+ if(stat.strings.containsKey(name)) {
+ LOGGER.warning(com.warn("Shadowing string '%s'\n", name));
+ }
+
+ stat.strings.put(name, strang);
+
+ return SUCCESS;
+ }
+
+ private CommandStatus defFormat(Command com) {
+ String remn = com.remnCommand;
+
+ int idx = remn.indexOf(' ');
+ if(idx == -1) {
+ LOGGER.warning(com.warn("Binding empty format to name '%s'\n", remn));
+ idx = remn.length();
+ }
+ String name = remn.substring(0, idx);
+ String fmt = remn.substring(idx);
+
+ if(stat.formats.containsKey(name)) {
+ LOGGER.warning(com.warn("Shadowing format '%s'\n", name));
+ }
+
+ stat.formats.put(name, fmt);
+
+ return SUCCESS;
+ }
+
+ private CommandStatus bindFormat(Command com) {
+ String[] parts = com.remnCommand.split(" ");
+
+ return SUCCESS;
+ }
+}