diff options
| author | Benjamin J. Culkin <bjculkin@mix.wvu.edu> | 2017-10-08 22:39:59 -0300 |
|---|---|---|
| committer | Benjamin J. Culkin <bjculkin@mix.wvu.edu> | 2017-10-08 22:39:59 -0300 |
| commit | c82e3b3b2de0633317ec8fc85925e91422820597 (patch) | |
| tree | 96567416ce23c5ce85601f9cedc3a94bb1c55cba /base/src/main/java/bjc/utils/cli | |
| parent | b3ac1c8690c3e14c879913e5dcc03a5f5e14876e (diff) | |
Start splitting into maven modules
Diffstat (limited to 'base/src/main/java/bjc/utils/cli')
| -rw-r--r-- | base/src/main/java/bjc/utils/cli/CLICommander.java | 134 | ||||
| -rw-r--r-- | base/src/main/java/bjc/utils/cli/Command.java | 39 | ||||
| -rw-r--r-- | base/src/main/java/bjc/utils/cli/CommandHandler.java | 24 | ||||
| -rw-r--r-- | base/src/main/java/bjc/utils/cli/CommandHelp.java | 31 | ||||
| -rw-r--r-- | base/src/main/java/bjc/utils/cli/CommandMode.java | 72 | ||||
| -rw-r--r-- | base/src/main/java/bjc/utils/cli/DelegatingCommand.java | 64 | ||||
| -rw-r--r-- | base/src/main/java/bjc/utils/cli/GenericCommand.java | 84 | ||||
| -rw-r--r-- | base/src/main/java/bjc/utils/cli/GenericCommandMode.java | 469 | ||||
| -rw-r--r-- | base/src/main/java/bjc/utils/cli/GenericHelp.java | 63 | ||||
| -rw-r--r-- | base/src/main/java/bjc/utils/cli/NullHelp.java | 20 | ||||
| -rw-r--r-- | base/src/main/java/bjc/utils/cli/objects/BlockReaderCLI.java | 392 | ||||
| -rw-r--r-- | base/src/main/java/bjc/utils/cli/objects/Command.java | 87 | ||||
| -rw-r--r-- | base/src/main/java/bjc/utils/cli/objects/DefineCLI.java | 133 |
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> + * "<command-name>\t<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; + } +} |
