/** * 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; /** * The concatination of {@link #files} with blankspaces as delimiters, {@code null} if no files */ public String message = null; /** * 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 > 0) { 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).replace('\t', ' ').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 \0" + last; l += first.length() + 6 + last.length(); if (opt.getClass() == Variadic.class) { line += " [\033[04m" + opt.argument + "\033[24m...]"; l += opt.argument.length() + 6; } else if (opt.getClass() Argumented.class) { line += " \033[04m" + opt.argument + "\033[24m"; l += opt.argument.length() + 1; } lines.add(line); lens.add(new int[] { l }); } int col = 0; for (final int[] len : lens) if (col < len[0]) col = len[0]; 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("\0", "\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; this.print(line + "\033[00m\n"); } else this.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); } int i = 0, n = optqueue.size(); while (i < n) { final String opt = this.optmap.get(optqueue.get(i)).standard; final String arg = argqueue.size() > i ? argqueue.get(i) : null; i++; if (this.opts.get(opt) == null) this.opts.put(opt, new String[] {}); if (argqueue.size() >= i) this.opts.put(opt, append(this.opts.get(opt), arg)); } for (final Option opt : this.options) if (opt.getClass() == Variadic.class) { final String[] varopt = this.opts.get(opt.standard); if (varopt != null) { final String[] additional = new String[this.files.size()]; this.files.toArray(additional); if (varopt[0] == null) this.opts.put(opt.standard, additional); else this.opts.put(opt.standard, append(varopt, additional)); this.files.clear(); break; } } final StringBuilder sb = new StringBuilder(); for (final String file : this.files) { sb.append(' '); sb.append(file); } this.message = sb.toString().substring(1); if (this.unrecognisedCount > 5) { int more = this.unrecognisedCount - 5; this.print(this.program + ": warning: " + more + " more unrecognised "); this.println(more == 1 ? "option" : "options"); } return rc; } /** * Create a new identical array, except with extra items at the end * * @param array The array * @param items The new items * @return The new array */ private String[] append(final String[] array, final String... items) { final String[] rc = new String[array.length + items.length]; System.arraycopy(array, 0, rc, 0, array.length); System.arraycopy(items, 0, rc, array.length, items.length); return rc; } }