diff options
Diffstat (limited to 'src/ArgParser.java')
-rw-r--r-- | src/ArgParser.java | 1007 |
1 files changed, 1007 insertions, 0 deletions
diff --git a/src/ArgParser.java b/src/ArgParser.java new file mode 100644 index 0000000..495e07c --- /dev/null +++ b/src/ArgParser.java @@ -0,0 +1,1007 @@ +/** + * argparser – command line argument parser library + * + * Copyright © 2013 Mattias Andrée (maandree@member.fsf.org) + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this library. If not, see <http://www.gnu.org/licenses/>. + */ +import java.util.*; +import java.io.*; + + +/** + * Simple argument parser + * + * @author Mattias Andrée, <a href="mailto:maandree@member.fsf.org">maandree@member.fsf.org</a> + */ +public class ArgParser +{ + /** + * <p>Constructor</p> + * <p> + * The short description is printed on same line as the program name + * </p> + * + * @param description Short, single-line, description of the program + * @param usage Formated, multi-line, usage text, may be {@code null} + */ + public ArgParser(final String description, final String usage) + { this(description, usage, null, null, false); + } + + /** + * <p>Constructor</p> + * <p> + * The short description is printed on same line as the program name + * </p> + * + * @param description Short, single-line, description of the program + * @param usage Formated, multi-line, usage text, may be {@code null} + * @param useStderr Whether to use stderr instead of stdout + */ + public ArgParser(final String description, final String usage, final boolean useStderr) + { this(description, usage, null, null, useStderr); + } + + /** + * <p>Constructor</p> + * <p> + * The short description is printed on same line as the program name + * </p> + * + * @param description Short, single-line, description of the program + * @param usage Formated, multi-line, usage text, may be {@code null} + * @param longDescription Long, multi-line, description of the program, may be {@code null} + */ + public ArgParser(final String description, final String usage, final String longDescription) + { this(description, usage, longDescription, null, false); + } + + /** + * <p>Constructor</p> + * <p> + * The short description is printed on same line as the program name + * </p> + * + * @param description Short, single-line, description of the program + * @param usage Formated, multi-line, usage text, may be {@code null} + * @param longDescription Long, multi-line, description of the program, may be {@code null} + * @param useStderr Whether to use stderr instead of stdout + */ + public ArgParser(final String description, final String usage, final String longDescription, final boolean useStderr) + { this(description, usage, longDescription, null, useStderr); + } + + /** + * <p>Constructor</p> + * <p> + * The short description is printed on same line as the program name + * </p> + * + * @param description Short, single-line, description of the program + * @param usage Formated, multi-line, usage text, may be {@code null} + * @param longDescription Long, multi-line, description of the program, may be {@code null} + * @param program The name of the program, {@code null} for automatic + */ + public ArgParser(final String description, final String usage, final String longDescription, final String program) + { this(description, usage, longDescription, program, false); + } + + /** + * <p>Constructor</p> + * <p> + * The short description is printed on same line as the program name + * </p> + * + * @param description Short, single-line, description of the program + * @param usage Formated, multi-line, usage text, may be {@code null} + * @param longDescription Long, multi-line, description of the program, may be {@code null} + * @param program The name of the program, {@code null} for automatic + * @param useStderr Whether to use stderr instead of stdout + */ + public ArgParser(final String description, final String usage, final String longDescription, final String program, final boolean useStderr) + { + this.linuxvt = System.getenv("TERM") == null ? false : System.getenv("TERM").equals("linux"); + String prog = program == null ? ArgParser.parentName(0, true) : program; + this.program = prog == null ? "?" : prog; + this.description = description; + this.usage = usage; + this.longDescription = longDescription; + this.out = useStderr ? System.err : System.out; + } + + + + /** + * Whether the Linux VT is being used + */ + public boolean linuxvt; + + /** + * The name of the executed command + */ + public final String program; + + /** + * Short, single-line, description of the program + */ + private final String description; + + /** + * Formated, multi-line, usage text, {@code null} if none + */ + private final String usage; + + /** + * Long, multi-line, description of the program, {@code null} if none + */ + private final String longDescription; + + /** + * The error output stream + */ + private final OutputStream out; + + /** + * The passed arguments + */ + public String[] arguments = null; + + /** + * The number of unrecognised arguments + */ + public int unrecognisedCount = 0; + + /** + * Options, in order + */ + private final ArrayList<Option> options = new ArrayList<Option>(); + + /** + * Option map + */ + public final HashMap<String, Option> optmap = new HashMap<String, Option>(); + + /** + * The arguments passed that is not tied to an option + */ + public final ArrayList<String> files = new ArrayList<String>(); + + /** + * Parsed arguments, a map from option to arguments, {@code null} if not used, + * add one {@code null} element per argumentless use. + */ + public final HashMap<String, String[]> opts = new HashMap<String, String[]>(); + + + + /** + * Option class + */ + public class Option + { + /** + * Constructor + * + * @param alternatives Alterative option names + * @param standard Standard option index + * @param argument Argument name, not for argumentless options + */ + protected Option(final String[] alternatives, final int standard, final String argument) + { + this.alternatives = alternatives; + this.standard = alternatives[standard < 0 ? (alternatives.length + standard) : standard]; + this.argument = argument == null ? "ARG" : argument; + } + + + + /** + * Alterative option names + */ + public final String[] alternatives; + + /** + * Standard option name + */ + public final String standard; + + /** + * Argument name, not for argumentless options + */ + public final String argument; + + /** + * Help text, multi-line + */ + public String help = null; + } + + + /** + * Option takes no arguments + */ + public class Argumentless extends Option + { + /** + * Constructor + * + * @param alternatives Alterative option names + * @param standard Standard option index + */ + public Argumentless(final String[] alternatives, final int standard) + { super(alternatives, standard, null); + } + + /** + * Constructor + * + * @param standard Standard option index + * @param alternatives Alterative option names + */ + public Argumentless(final int standard, final String... alternatives) + { super(alternatives, standard, null); + } + + /** + * Constructor + * + * @param alternatives Alterative option names + */ + public Argumentless(final String... alternatives) + { super(alternatives, 0, null); + } + } + + + /** + * Option takes one argument per instance + */ + public class Argumented extends Option + { + /** + * Constructor + * + * @param alternatives Alterative option names + * @param standard Standard option index + * @param argument Argument name + */ + public Argumented(final String[] alternatives, final int standard, final String argument) + { super(alternatives, standard, argument); + } + + /** + * Constructor + * + * @param alternatives Alterative option names + * @param argument Argument name + * @param standard Standard option index + */ + public Argumented(final String[] alternatives, final String argument, final int standard) + { super(alternatives, standard, argument); + } + + /** + * Constructor + * + * @param alternatives Alterative option names + * @param argument Argument name + */ + public Argumented(final String[] alternatives, final String argument) + { super(alternatives, 0, argument); + } + + /** + * Constructor + * + * @param standard Standard option index + * @param argument Argument name + * @param alternatives Alterative option names + */ + public Argumented(final int standard, final String argument, final String... alternatives) + { super(alternatives, standard, argument); + } + + /** + * Constructor + * + * @param argument Argument name + * @param standard Standard option index + * @param alternatives Alterative option names + */ + public Argumented(final String argument, final int standard, final String... alternatives) + { super(alternatives, standard, argument); + } + + /** + * Constructor + * + * @param argument Argument name + * @param alternatives Alterative option names + */ + public Argumented(final String argument, final String... alternatives) + { super(alternatives, 0, argument); + } + } + + + /** + * Option consumes all following arguments + */ + public class Variadic extends Argumented + { + /** + * Constructor + * + * @param alternatives Alterative option names + * @param standard Standard option index + * @param argument Argument name + */ + public Variadic(final String[] alternatives, final int standard, final String argument) + { super(alternatives, standard, argument); + } + + /** + * Constructor + * + * @param alternatives Alterative option names + * @param argument Argument name + * @param standard Standard option index + */ + public Variadic(final String[] alternatives, final String argument, final int standard) + { super(alternatives, argument, standard); + } + + /** + * Constructor + * + * @param alternatives Alterative option names + * @param argument Argument name + */ + public Variadic(final String[] alternatives, final String argument) + { super(alternatives, argument); + } + + /** + * Constructor + * + * @param standard Standard option index + * @param argument Argument name + * @param alternatives Alterative option names + */ + public Variadic(final int standard, final String argument, final String... alternatives) + { super(standard, argument, alternatives); + } + + /** + * Constructor + * + * @param argument Argument name + * @param standard Standard option index + * @param alternatives Alterative option names + */ + public Variadic(final String argument, final int standard, final String... alternatives) + { super(argument, standard, alternatives); + } + + /** + * Constructor + * + * @param argument Argument name + * @param alternatives Alterative option names + */ + public Variadic(final String argument, final String... alternatives) + { super(argument, alternatives); + } + } + + + + /** + * Gets the name of the parent process + * + * @return The name of the parent process + */ + public static String parentName() + { + return ArgParser.parentName(1, false); + } + + /** + * Gets the name of the parent process + * + * @param levels The number of parents to walk, 0 for self, and 1 for direct parent + * @return The name of the parent process + */ + public static String parentName(final int levels) + { + return ArgParser.parentName(levels, false); + } + + /** + * Gets the name of the parent process + * + * @param hasInterpretor Whether the parent process is an interpretor + * @return The name of the parent process + */ + public static String parentName(final boolean hasInterpretor) + { + return ArgParser.parentName(1, hasInterpretor); + } + + /** + * Gets the name of the parent process + * + * @param levels The number of parents to walk, 0 for self, and 1 for direct parent + * @param hasInterpretor Whether the parent process is an interpretor + * @return The name of the parent process + */ + public static String parentName(final int levels, final boolean hasInterpretor) + { + int pid; + try + { pid = Integer.parseInt((new File("/proc/self")).getCanonicalPath().substring(6)); + } + catch (final Throwable err) + { return null; + } + int lvl = levels; + try + { outer: + while (lvl > 1) + { + InputStream is = null; + try + { is = new FileInputStream(new File("/proc/" + pid + "/status")); + byte[] data = new byte[is.available()]; + int off = 0; + while (off != data.length) + off += is.read(data, off, data.length - off); + String[] lines = (new String(data, "UTF-8")).split("\n"); + for (String line : lines) + { if (line.startsWith("PPid:")) + { + line = line.substring(5); + line = line.replace('\t', ' ').replace('\n', ' ').replace(" ", ""); + pid = Integer.parseInt(line); + lvl -= 1; + continue outer; + } } + return null; + } + finally + { if (is != null) + try + { is.close(); + } + catch (final Throwable ignore) + { /* ignore */ + } } + } + InputStream is = null; + try + { is = new FileInputStream(new File("/proc/" + pid + "/cmdline")); + byte[] data = new byte[is.available()]; + int off = 0; + while (off != data.length) + off += is.read(data, off, data.length - off); + String[] cmdline = new String(data, 0, off - 1, "UTF-8").split("\0"); + if (hasInterpretor == false) + { String rc = cmdline[0]; + return rc.length() == 0 ? null : rc; + } + boolean dashed = false; + for (int i = 1, n = cmdline.length; i < n; i++) + { + if (dashed) + return cmdline[i]; + if (cmdline[i].equals("--")) + dashed = true; + else if (cmdline[i].equals("-cp") || cmdline[i].equals("-classpath")) + i++; + else if (cmdline[i].startsWith("-") == false) + return cmdline[i]; + } + } + finally + { if (is != null) + try + { is.close(); + } + catch (final Throwable ignore) + { /* ignore */ + } } + return null; + } + catch (final Throwable err) + { return null; + } + } + + + /** + * Print an empty line to the selected error channel + */ + private void println() + { + this.print("\n", true); + } + + /** + * Print an empty line to the selected error channel + * + * @param flush Whether to flush the stream + */ + private void println(final boolean flush) + { + this.print("\n", flush); + } + + /** + * Print a text with an added line break to the selected error channel + */ + private void println(final String text) + { + this.print(text + "\n", true); + } + + /** + * Print a text with an added line break to the selected error channel + * + * @param flush Whether to flush the stream + */ + private void println(final String text, final boolean flush) + { + this.print(text + "\n", flush); + } + + /** + * Print a text to the selected error channel + */ + private void print(final String text) + { + this.print(text, false); + } + + /** + * Print a text to the selected error channel + * + * @param flush Whether to flush the stream + */ + private void print(final String text, final boolean flush) + { + try + { if (text != null) + this.out.write(text.getBytes("UTF-8")); + if (flush) + this.out.flush(); + } + catch (final Throwable ignore) + { /* ignore */ + } + } + + + /** + * Add an option + * + * @param option The option + */ + public void add(final Option option) + { + this.options.add(option); + for (final String alternative : option.alternatives) + this.optmap.put(alternative, option); + this.opts.put(option.standard, null); + } + + /** + * Add an option + * + * @param option The option + * @param help Help text, multi-line + */ + public void add(final Option option, final String help) + { + this.add(help, option); + } + + /** + * Add an option + * + * @param help Help text, multi-line + * @param option The option + */ + public void add(final String help, final Option option) + { + this.add(option); + option.help = help; + } + + + /** + * Maps up options that are alternatives to the first alternative for each option + */ + public void supportAlternatives() + { + for (final String opt : this.optmap.keySet()) + this.opts.put(opt, this.opts.get(this.optmap.get(opt).standard)); + } + + + /** + * Checks for option conflicts + * + * @param exclusives Exclusive options + * @param exitValue The value to exit with on the check does not pass, + * @return Whether at most one exclusive option was used + */ + public boolean testExclusiveness(final Set<String> exclusives, final int exitValue) + { + boolean rc = this.testExclusiveness(exclusives); + if (rc == false) + System.exit(exitValue); + return rc; + } + + /** + * Checks for option conflicts + * + * @param exclusives Exclusive options + * @return Whether at most one exclusive option was used + */ + public boolean testExclusiveness(final Set<String> exclusives) + { + final ArrayList<String> used = new ArrayList<String>(); + + for (final String opt : this.opts.keySet()) + if ((this.opts.get(opt) != null) && exclusives.contains(opt)) + used.add(opt); + + if (used.size() > 1) + { String msg = this.program + ": conflicting options:"; + for (final String opt : used) + if (this.optmap.get(opt).standard.equals(opt)) + msg += " " + opt; + else + msg += " " + opt + "(" + this.optmap.get(opt).standard + ")"; + this.println(msg, true); + return false; + } + return true; + } + + + /** + * Checks for out of context option usage + * + * @param allowed Allowed options + * @param exitValue The value to exit with on the check does not pass, + * @return Whether only allowed options was used + */ + public boolean testAllowed(final Set<String> allowed, final int exitValue) + { + boolean rc = this.testAllowed(allowed); + if (rc == false) + System.exit(exitValue); + return rc; + } + + /** + * Checks for out of context option usage + * + * @param allowed Allowed options + * @return Whether only allowed options was used + */ + public boolean testAllowed(final Set<String> allowed) + { + boolean rc = true; + for (final String opt : this.opts.keySet()) + if ((this.opts.get(opt) != null) && (allowed.contains(opt) == false)) + { String msg = this.program + ": option used out of context: " + opt; + if (opt.equals(this.optmap.get(opt).standard) == false) + msg += "(" + this.optmap.get(opt).standard + ")"; + this.println(msg, true); + rc = false; + } + return rc; + } + + + /** + * Checks the correctness of the number of used non-option arguments + * + * @param min The minimum number of files + * @param exitValue The value to exit with on the check does not pass + * @return Whether the usage was correct + */ + public boolean testFilesMin(final int min, final int exitValue) + { + boolean rc = this.testFilesMin(min); + if (rc == false) + System.exit(exitValue); + return rc; + } + + /** + * Checks the correctness of the number of used non-option arguments + * + * @param min The minimum number of files + * @return Whether the usage was correct + */ + public boolean testFilesMin(final int min) + { + return min <= this.files.size(); + } + + /** + * Checks the correctness of the number of used non-option arguments + * + * @param max The maximum number of files + * @param exitValue The value to exit with on the check does not pass + * @return Whether the usage was correct + */ + public boolean testFilesMax(final int max, final int exitValue) + { + boolean rc = this.testFilesMax(max); + if (rc == false) + System.exit(exitValue); + return rc; + } + + /** + * Checks the correctness of the number of used non-option arguments + * + * @param max The maximum number of files + * @return Whether the usage was correct + */ + public boolean testFilesMax(final int max) + { + return this.files.size() <= max; + } + + /** + * Checks the correctness of the number of used non-option arguments + * + * @param min The minimum number of files + * @param max The maximum number of files + * @param exitValue The value to exit with on the check does not pass + * @return Whether the usage was correct + */ + public boolean testFiles(final int min, final int max, final int exitValue) + { + boolean rc = this.testFiles(min, max); + if (rc == false) + System.exit(exitValue); + return rc; + } + + /** + * Checks the correctness of the number of used non-option arguments + * + * @param min The minimum number of files + * @param max The maximum number of files + * @return Whether the usage was correct + */ + public boolean testFiles(final int min, final int max) + { + return (min <= this.files.size()) && (this.files.size() <= max); + } + + + /** + * Prints a colourful help message + */ + public void help() + { + final String dash = this.linuxvt ? "-" : "—"; + this.println("\033[01m" + program + "\033[21m " + dash + " " + this.description + "\n", false); + if (this.longDescription != null) + this.println(longDescription, false); + this.println(false); + + if (this.usage != null) + { this.print("\033[01mUSAGE:\033[21m"); + boolean first = true; + for (final String line : this.usage.split("\n")) + { if (first) + first = false; + else + this.print(" or"); + this.println("\t" + line, false); + } + this.println(false); + } + + int maxfirstlen = 0; + for (final Option opt : this.options) + { if (opt.help == null) + continue; + if (opt.alternatives.length > 1) + if (maxfirstlen < opt.alternatives[0].length()) + maxfirstlen = opt.alternatives[0].length(); + } + String empty = " "; + while (empty.length() < maxfirstlen) + empty += empty; + empty = empty.substring(0, maxfirstlen); + + this.println("\033[01mSYNOPSIS:\033[21m", false); + final ArrayList<String> lines = new ArrayList<String>(); + final ArrayList<int[]> lens = new ArrayList<int[]>(); + for (final Option opt : this.options) + { if (opt.help == null) + continue; + int l = 0; + String first = opt.alternatives[0]; + String last = opt.alternatives[opt.alternatives.length - 1]; + if (first == last) + first = empty; + else + first += empty.substring(first.length()); + String line = " \033[02m" + first + "\033[22m %colour%" + last; + l += first.length() + 6 + last.length(); + if (opt instanceof Variadic) + { line += " [\033[04m" + opt.argument + "\033[24m...]"; + l += opt.argument.length() + 6; + } + else if (opt instanceof Argumented) + { line += " \033[04m" + opt.argument + "\033[24m"; + l += opt.argument.length() + 1; + } + lines.add(line); + lens.add(new int[] { l }); + } + + int col = lens.size(); + col += 8 - ((col - 4) & 7); + int index = 0; + while (empty.length() < col) + empty += empty; + empty = empty.substring(0, col); + for (final Option opt : this.options) + { if (opt.help == null) + continue; + boolean first = true; + final String colour = (index & 1) == 0 ? "36" : "34"; + { String line = lines.get(index).replace("%colour%", "\033[" + colour + ";01m"); + line += empty.substring(lens.get(index)[0]); + this.print(line, false); + } + for (final String line : opt.help.split("\n")) + if (first) + { first = false; + print(line + "\033[00m\n"); + } + else + print(empty + "\033[" + colour + "m" + line + "\033[00m\n"); + index++; + } + + this.println(true); + } + + + /** + * Parse arguments + * + * @param args The command line arguments, however it should not include the execute file at index 0 + * @return Whether no unrecognised option is used + */ + public boolean parse(final String[] argv) + { + this.arguments = argv; + + final ArrayList<String> argqueue = new ArrayList<String>(); + final ArrayList<String> optqueue = new ArrayList<String>(); + final ArrayList<String> queue = new ArrayList<String>(); + for (final String arg : argv) + queue.add(arg); + + boolean dashed = false, tmpdashed = false, rc = true; + int get = 0, dontget = 0; + + while (queue.size() > 0) + { final String arg = queue.remove(0); + if ((get > 0) && (dontget == 0)) + { get--; + argqueue.add(arg); + } + else if (tmpdashed) + { this.files.add(arg); + tmpdashed = false; + } + else if (dashed) this.files.add(arg); + else if (arg.equals("++")) tmpdashed = true; + else if (arg.equals("--")) dashed = true; + else if ((arg.length() > 1) && ((arg.charAt(0) == '-') || (arg.charAt(0) == '+'))) + if ((arg.length() > 2) && (arg.charAt(1) == arg.charAt(0))) + { Option opt = this.optmap.get(arg); + if (dontget > 0) + dontget--; + else if ((opt != null) && (opt.getClass() == Argumentless.class)) + { optqueue.add(arg); + argqueue.add(null); + } + else if (arg.contains("=")) + { String arg_opt = arg.substring(0, arg.indexOf('=')); + Option arg_opt_opt = this.optmap.get(arg_opt); + if ((arg_opt_opt != null) && (arg_opt_opt instanceof Argumented)) + { optqueue.add(arg_opt); + argqueue.add(arg.substring(arg.indexOf('=') + 1)); + if (arg_opt_opt instanceof Variadic) + dashed = true; + } + else + { if (++this.unrecognisedCount <= 5) + this.println(this.program + ": warning: unrecognised option " + arg, true); + rc = false; + } + } + else if ((opt != null) && (opt.getClass() == Argumented.class)) + { optqueue.add(arg); + get++; + } + else if ((opt != null) && (opt.getClass() == Variadic.class)) + { optqueue.add(arg); + argqueue.add(null); + dashed = true; + } + else + { if (++this.unrecognisedCount <= 5) + this.println(this.program + ": warning: unrecognised option " + arg, true); + rc = false; + } + } + else + { String sign = String.valueOf(arg.charAt(0)), narg; + int i = 1, n = arg.length(); + while (i < n) + { Option opt = this.optmap.get(narg = sign + arg.charAt(i++)); + if (opt != null) + if (opt.getClass() == Argumentless.class) + { optqueue.add(narg); + argqueue.add(null); + } + else if (opt.getClass() == Argumented.class) + { optqueue.add(narg); + String nargarg = arg.substring(i); + if (nargarg.length() == 0) + get++; + else + argqueue.add(nargarg); + break; + } + else + { optqueue.add(narg); + String nargarg = arg.substring(i); + argqueue.add(nargarg.length() > 0 ? nargarg : null); + dashed = true; + break; + } + else + { if (++this.unrecognisedCount <= 5) + this.println(this.program + ": warning: unrecognised option " + arg, true); + rc = false; + } + } } + else + this.files.add(arg); + } + + return rc; + } + +} + |