/* * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) * Copyright (C) $$Year-Rel$$ The Jalview Authors * * This file is part of Jalview. * * Jalview 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. * * Jalview 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 Jalview. If not, see . * The Jalview Authors are detailed in the 'AUTHORS' file. */ package jalview.bin; import java.io.File; import java.io.IOException; import java.net.URLDecoder; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import jalview.util.FileUtils; public class ArgParser { private static final String DOUBLEDASH = "--"; private static final String NEGATESTRING = "no"; // the linked id used for no id (not even square braces) private static final String DEFAULTLINKEDID = ""; // the linked id used to increment the idCounter (and use the incremented // value) private static final String INCREMENTAUTOCOUNTERLINKEDID = "{++n}"; // the linked id used to use the idCounter private static final String AUTOCOUNTERLINKEDID = "{n}"; private int idCounter = 0; private static enum Opt { BOOLEAN, STRING, UNARY, MULTI, LINKED, NODUPLICATEVALUES, BOOTSTRAP, GLOB } public enum Arg { /* NOCALCULATION, NOMENUBAR, NOSTATUS, SHOWOVERVIEW, ANNOTATIONS, COLOUR, FEATURES, GROOVY, GROUPS, HEADLESS, JABAWS, NOANNOTATION, NOANNOTATION2, NODISPLAY, NOGUI, NONEWS, NOQUESTIONNAIRE, NOSORTBYTREE, NOUSAGESTATS, OPEN, OPEN2, PROPS, QUESTIONNAIRE, SETPROP, SORTBYTREE, TREE, VDOC, VSESS; */ HELP("h"), CALCULATION, MENUBAR, STATUS, SHOWOVERVIEW, ANNOTATIONS, COLOUR, FEATURES, GROOVY, GROUPS, HEADLESS, JABAWS, ANNOTATION, ANNOTATION2, DISPLAY, GUI, NEWS, NOQUESTIONNAIRE, SORTBYTREE, USAGESTATS, OPEN, OPEN2, PROPS, QUESTIONNAIRE, SETPROP, TREE, VDOC, VSESS, OUTPUT, OUTPUTTYPE, SSANNOTATION, NOTEMPFAC, TEMPFAC, TEMPFAC_LABEL, TEMPFAC_DESC, TEMPFAC_SHADING, TITLE, PAEMATRIX, WRAP, NOSTRUCTURE, STRUCTURE, IMAGE, QUIT, CLOSE, DEBUG("d"), QUIET("q"), ARGFILE; static { HELP.setOptions(Opt.UNARY); CALCULATION.setOptions(true, Opt.BOOLEAN); // default "true" implies only // expecting "--nocalculation" MENUBAR.setOptions(true, Opt.BOOLEAN); STATUS.setOptions(true, Opt.BOOLEAN); SHOWOVERVIEW.setOptions(Opt.UNARY, Opt.LINKED); ANNOTATIONS.setOptions(Opt.STRING, Opt.LINKED); COLOUR.setOptions(Opt.STRING, Opt.LINKED); FEATURES.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI); GROOVY.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI); GROUPS.setOptions(Opt.STRING, Opt.LINKED); HEADLESS.setOptions(Opt.UNARY, Opt.BOOTSTRAP); JABAWS.setOptions(Opt.STRING); ANNOTATION.setOptions(true, Opt.BOOLEAN); ANNOTATION2.setOptions(true, Opt.BOOLEAN); DISPLAY.setOptions(true, Opt.BOOLEAN); GUI.setOptions(true, Opt.BOOLEAN); NEWS.setOptions(true, Opt.BOOLEAN); NOQUESTIONNAIRE.setOptions(Opt.UNARY); // unary as --questionnaire=val // expects a string value SORTBYTREE.setOptions(true, Opt.BOOLEAN); USAGESTATS.setOptions(true, Opt.BOOLEAN); OPEN.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI, Opt.GLOB); OPEN2.setOptions(Opt.STRING, Opt.LINKED); PROPS.setOptions(Opt.STRING, Opt.BOOTSTRAP); QUESTIONNAIRE.setOptions(Opt.STRING); SETPROP.setOptions(Opt.STRING); TREE.setOptions(Opt.STRING); VDOC.setOptions(Opt.UNARY); VSESS.setOptions(Opt.UNARY); OUTPUT.setOptions(Opt.STRING, Opt.LINKED); OUTPUTTYPE.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI); SSANNOTATION.setOptions(Opt.BOOLEAN, Opt.LINKED); NOTEMPFAC.setOptions(Opt.UNARY, Opt.LINKED); TEMPFAC.setOptions(Opt.STRING, Opt.LINKED); TEMPFAC_LABEL.setOptions(Opt.STRING, Opt.LINKED); TEMPFAC_DESC.setOptions(Opt.STRING, Opt.LINKED); TEMPFAC_SHADING.setOptions(Opt.BOOLEAN, Opt.LINKED); TITLE.setOptions(Opt.STRING, Opt.LINKED); PAEMATRIX.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI); NOSTRUCTURE.setOptions(Opt.UNARY, Opt.LINKED); STRUCTURE.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI); WRAP.setOptions(Opt.BOOLEAN, Opt.LINKED); IMAGE.setOptions(Opt.STRING, Opt.LINKED); QUIT.setOptions(Opt.UNARY); CLOSE.setOptions(Opt.UNARY, Opt.LINKED); DEBUG.setOptions(Opt.BOOLEAN, Opt.BOOTSTRAP); QUIET.setOptions(Opt.UNARY, Opt.MULTI, Opt.BOOTSTRAP); ARGFILE.setOptions(Opt.STRING, Opt.MULTI, Opt.BOOTSTRAP, Opt.GLOB); // BOOTSTRAP args are parsed before a full parse of arguments and // so are accessible at an earlier stage to (e.g.) set debug log level, // provide a props file (that might set log level), run headlessly, read // an argfile instead of other args. } private final String[] argNames; private Opt[] argOptions; private boolean defaultBoolValue = false; public String toLongString() { StringBuilder sb = new StringBuilder(); sb.append("Arg: ").append(this.name()); for (String name : getNames()) { sb.append(", '").append(name).append("'"); } sb.append("\nOptions: "); boolean first = true; for (Opt o : argOptions) { if (!first) { sb.append(", "); } sb.append(o.toString()); first = false; } sb.append("\n"); return sb.toString(); } private Arg() { this(new String[0]); } private Arg(String... names) { int length = (names == null || names.length == 0 || (names.length == 1 && names[0] == null)) ? 1 : names.length + 1; this.argNames = new String[length]; this.argNames[0] = this.getName(); if (length > 1) System.arraycopy(names, 0, this.argNames, 1, names.length); } public String[] getNames() { return argNames; } public String getName() { return this.name().toLowerCase(Locale.ROOT).replace('_', '-'); } @Override public final String toString() { return getName(); } public boolean hasOption(Opt o) { if (argOptions == null) return false; for (Opt option : argOptions) { if (o == option) return true; } return false; } protected void setOptions(Opt... options) { setOptions(false, options); } protected void setOptions(boolean defaultBoolValue, Opt... options) { this.defaultBoolValue = defaultBoolValue; argOptions = options; } protected boolean getDefaultBoolValue() { return defaultBoolValue; } } public static class ArgValues { private static final String ID = "id"; private Arg arg; private int argCount = 0; private boolean boolValue = false; private boolean negated = false; private int boolIndex = -1; private List argsIndexes; private List argValueList; private Map idMap = new HashMap<>(); protected ArgValues(Arg a) { this.arg = a; this.argValueList = new ArrayList(); this.boolValue = arg.getDefaultBoolValue(); } public Arg arg() { return arg; } protected int getCount() { return argCount; } protected void incrementCount() { argCount++; } protected void setNegated(boolean b) { this.negated = b; } protected boolean isNegated() { return this.negated; } protected void setBoolean(boolean b, int i) { this.boolValue = b; this.boolIndex = i; } protected boolean getBoolean() { return this.boolValue; } @Override public String toString() { if (argValueList == null) return null; StringBuilder sb = new StringBuilder(); sb.append(arg.toLongString()); if (arg.hasOption(Opt.BOOLEAN) || arg.hasOption(Opt.UNARY)) sb.append("Boolean: ").append(boolValue).append("; Default: ") .append(arg.getDefaultBoolValue()).append("; Negated: ") .append(negated).append("\n"); if (arg.hasOption(Opt.STRING)) { sb.append("Values:"); boolean first = true; for (ArgValue av : argValueList) { String v = av.getValue(); if (!first) sb.append(","); sb.append("\n '"); sb.append(v).append("'"); first = false; } sb.append("\n"); } sb.append("Count: ").append(argCount).append("\n"); return sb.toString(); } protected void addValue() { addValue(null, -1); } protected void addValue(String val, int argIndex) { addArgValue(new ArgValue(val, argIndex)); } protected void addArgValue(ArgValue av) { if ((!arg.hasOption(Opt.MULTI) && argValueList.size() > 0) || (arg.hasOption(Opt.NODUPLICATEVALUES) && argValueList.contains(av.getValue()))) return; if (argValueList == null) { argValueList = new ArrayList(); } SubVals sv = ArgParser.getSubVals(av.getValue()); if (sv.has(ID)) { String id = sv.get(ID); av.setId(id); idMap.put(id, av); } argValueList.add(av); } protected boolean hasValue(String val) { return argValueList.contains(val); } protected ArgValue getArgValue() { if (arg.hasOption(Opt.MULTI)) Console.warn("Requesting single value for multi value argument"); return argValueList.size() > 0 ? argValueList.get(0) : null; } protected List getArgValueList() { return argValueList; } protected boolean hasId(String id) { return idMap.containsKey(id); } protected ArgValue getId(String id) { return idMap.get(id); } } // old style private List vargs = null; private boolean isApplet; // private AppletParams appletParams; public boolean isApplet() { return isApplet; } public String nextValue() { return vargs.remove(0); } public int getSize() { return vargs.size(); } public String getValue(String arg) { return getValue(arg, false); } public String getValue(String arg, boolean utf8decode) { int index = vargs.indexOf(arg); String dc = null; String ret = null; if (index != -1) { ret = vargs.get(index + 1).toString(); vargs.remove(index); vargs.remove(index); if (utf8decode && ret != null) { try { dc = URLDecoder.decode(ret, "UTF-8"); ret = dc; } catch (Exception e) { // TODO: log failure to decode } } } return ret; } // new style private static final Map argMap; private Map linkedArgs = new HashMap<>(); private List linkedOrder = null; private List argList; static { argMap = new HashMap<>(); for (Arg a : EnumSet.allOf(Arg.class)) { for (String argName : a.getNames()) { if (argMap.containsKey(argName)) { Console.warn("Trying to add argument name multiple times: '" + argName + "'"); // RESTORE THIS WHEN MERGED if (argMap.get(argName) != a) { Console.error( "Trying to add argument name multiple times for different Args: '" + argMap.get(argName).getName() + ":" + argName + "' and '" + a.getName() + ":" + argName + "'"); } continue; } argMap.put(argName, a); } } } public ArgParser(String[] args) { // make a mutable new ArrayList so that shell globbing parser works this(new ArrayList<>(Arrays.asList(args))); } public ArgParser(List args) { init(args); } private void init(List args) { // Enumeration argE = Collections.enumeration(args); int argIndex = 0; // while (argE.hasMoreElements()) for (int i = 0; i < args.size(); i++) { // String arg = argE.nextElement(); String arg = args.get(i); String argName = null; String val = null; List vals = null; // for Opt.GLOB only String linkedId = null; if (arg.startsWith(DOUBLEDASH)) { int equalPos = arg.indexOf('='); if (equalPos > -1) { argName = arg.substring(DOUBLEDASH.length(), equalPos); val = arg.substring(equalPos + 1); } else { argName = arg.substring(DOUBLEDASH.length()); } int idOpen = argName.indexOf('['); int idClose = argName.indexOf(']'); if (idOpen > -1 && idClose == argName.length() - 1) { linkedId = argName.substring(idOpen + 1, idClose); argName = argName.substring(0, idOpen); } Arg a = argMap.get(argName); // check for boolean prepended by "no" boolean negated = false; if (a == null && argName.startsWith(NEGATESTRING) && argMap .containsKey(argName.substring(NEGATESTRING.length()))) { argName = argName.substring(NEGATESTRING.length()); a = argMap.get(argName); negated = true; } // check for config errors if (a == null) { // arg not found Console.error("Argument '" + arg + "' not recognised. Ignoring."); continue; } if (!a.hasOption(Opt.BOOLEAN) && negated) { // used "no" with a non-boolean option Console.error("Argument '--" + NEGATESTRING + argName + "' not a boolean option. Ignoring."); continue; } if (!a.hasOption(Opt.STRING) && equalPos > -1) { // set --argname=value when arg does not accept values Console.error("Argument '--" + argName + "' does not expect a value (given as '" + arg + "'). Ignoring."); continue; } if (!a.hasOption(Opt.LINKED) && linkedId != null) { // set --argname[linkedId] when arg does not use linkedIds Console.error("Argument '--" + argName + "' does not expect a linked id (given as '" + arg + "'). Ignoring."); continue; } if (a.hasOption(Opt.STRING) && equalPos == -1) { // take next arg as value if required, and '=' was not found // if (!argE.hasMoreElements()) if (i + 1 >= args.size()) { // no value to take for arg, which wants a value Console.error("Argument '" + a.getName() + "' requires a value, none given. Ignoring."); continue; } // deal with bash globs here (--arg val* is expanded before reaching // the JVM). Note that SubVals cannot be used in this case. // If using the --arg=val then the glob is preserved and Java globs // will be used later. SubVals can be used. if (a.hasOption(Opt.GLOB)) { vals.addAll(getShellGlobbedFilenameValues(a, args, i + 1)); } else { val = args.get(i + 1); } } String autoCounterString = null; if (a.hasOption(Opt.LINKED)) { if (linkedId == null) { // use default linkedId for linked arguments linkedId = DEFAULTLINKEDID; Console.debug( "Changing linkedId to '" + linkedId + "' from " + arg); } else if (linkedId.equals(AUTOCOUNTERLINKEDID)) { // turn {n} to the autoCounter autoCounterString = Integer.toString(idCounter); linkedId = autoCounterString; Console.debug( "Changing linkedId to '" + linkedId + "' from " + arg); } else if (linkedId.equals(INCREMENTAUTOCOUNTERLINKEDID)) { // turn {++n} to the incremented autoCounter autoCounterString = Integer.toString(++idCounter); linkedId = autoCounterString; Console.debug( "Changing linkedId to '" + linkedId + "' from " + arg); } } if (!linkedArgs.containsKey(linkedId)) linkedArgs.put(linkedId, new ArgValuesMap()); ArgValuesMap avm = linkedArgs.get(linkedId); // not dealing with both NODUPLICATEVALUES and GLOB if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val)) { Console.error("Argument '--" + argName + "' cannot contain a duplicate value ('" + val + "'). Ignoring this and subsequent occurrences."); continue; } // check for unique id SubVals sv = ArgParser.getSubVals(val); String id = sv.get(ArgValues.ID); if (id != null && avm.hasId(a, id)) { Console.error("Argument '--" + argName + "' has a duplicate id ('" + id + "'). Ignoring."); continue; } ArgValues avs = avm.getOrCreateArgValues(a); if (avs == null) { avs = new ArgValues(a); } // store appropriate value if (a.hasOption(Opt.STRING)) { if (a.hasOption(Opt.GLOB) && vals != null && vals.size() > 0) { for (String v : vals) avs.addValue(val, argIndex++); } else { avs.addValue(val, argIndex); } } else if (a.hasOption(Opt.BOOLEAN)) { avs.setBoolean(!negated, argIndex); avs.setNegated(negated); } else if (a.hasOption(Opt.UNARY)) { avs.setBoolean(true, argIndex); } avs.incrementCount(); // store in appropriate place if (a.hasOption(Opt.LINKED)) { // allow a default linked id for single usage if (linkedId == null) linkedId = DEFAULTLINKEDID; // store the order of linkedIds if (linkedOrder == null) linkedOrder = new ArrayList<>(); if (!linkedOrder.contains(linkedId)) linkedOrder.add(linkedId); } // store arg in the list of args used if (argList == null) argList = new ArrayList<>(); if (!argList.contains(a)) argList.add(a); } } } /* * A helper method to take a list of String args where we're expecting * {"--previousargs", "--arg", "file1", "file2", "file3", "--otheroptionsornot"} * and the index of the globbed arg, here 1. It returns a * List {"file1", "file2", "file3"} * *and remove these from the original list object* so that processing * can continue from where it has left off, e.g. args has become * {"--previousargs", "--arg", "--otheroptionsornot"} * so the next increment carries on from the next --arg if available. */ private static List getShellGlobbedFilenameValues(Arg a, List args, int i) { List vals = new ArrayList<>(); while (i < args.size() && !args.get(i).startsWith(DOUBLEDASH)) { vals.add(args.remove(i)); if (!a.hasOption(Opt.GLOB)) break; } return vals; } public boolean isSet(Arg a) { return a.hasOption(Opt.LINKED) ? isSet("", a) : isSet(null, a); } public boolean isSet(String linkedId, Arg a) { ArgValuesMap avm = linkedArgs.get(linkedId); return avm == null ? false : avm.containsArg(a); } public boolean getBool(Arg a) { if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY)) { Console.warn("Getting boolean from non boolean Arg '" + a.getName() + "'."); } return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a); } public boolean getBool(String linkedId, Arg a) { ArgValuesMap avm = linkedArgs.get(linkedId); if (avm == null) return a.getDefaultBoolValue(); ArgValues avs = avm.getArgValues(a); return avs == null ? a.getDefaultBoolValue() : avs.getBoolean(); } public List linkedIds() { return linkedOrder; } public ArgValuesMap linkedArgs(String id) { return linkedArgs.get(id); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("UNLINKED\n"); sb.append(argValuesMapToString(linkedArgs.get(null))); if (linkedIds() != null) { sb.append("LINKED\n"); for (String id : linkedIds()) { // already listed these as UNLINKED args if (id == null) continue; ArgValuesMap avm = linkedArgs(id); sb.append("ID: '").append(id).append("'\n"); sb.append(argValuesMapToString(avm)); } } return sb.toString(); } private static String argValuesMapToString(ArgValuesMap avm) { if (avm == null) return null; StringBuilder sb = new StringBuilder(); for (Arg a : avm.getArgKeys()) { ArgValues v = avm.getArgValues(a); sb.append(v.toString()); sb.append("\n"); } return sb.toString(); } public static SubVals getSubVals(String item) { return new SubVals(item); } /** * A helper class to keep an index of argument position with argument values */ public static class ArgValue { private int argIndex; private String value; private String id; protected ArgValue(String value, int argIndex) { this.value = value; this.argIndex = argIndex; } protected String getValue() { return value; } protected int getArgIndex() { return argIndex; } protected void setId(String i) { id = i; } protected String getId() { return id; } } /** * A helper class to parse a string of the possible forms "content" * "[index]content", "[keyName=keyValue]content" and return the integer index, * the strings keyName and keyValue, and the content after the square brackets * (if present). Values not set `will be -1 or null. */ public static class SubVals { private static int NOTSET = -1; private int index = NOTSET; private Map subVals = null; private static char SEPARATOR = ';'; private String content = null; public SubVals(String item) { this.parseVals(item); } public void parseVals(String item) { if (item == null) return; if (item.indexOf('[') == 0 && item.indexOf(']') > 1) { int openBracket = item.indexOf('['); int closeBracket = item.indexOf(']'); String subvalsString = item.substring(openBracket + 1, closeBracket); this.content = item.substring(closeBracket + 1); boolean setIndex = false; for (String subvalString : subvalsString .split(Character.toString(SEPARATOR))) { int equals = subvalString.indexOf('='); if (equals > -1) { if (subVals == null) subVals = new HashMap<>(); subVals.put(subvalString.substring(0, equals), subvalString.substring(equals + 1)); } else { try { this.index = Integer.parseInt(subvalString); setIndex = true; } catch (NumberFormatException e) { Console.warn("Failed to obtain subvalue or index from '" + item + "'. Setting index=0 and using content='" + content + "'."); } } } if (!setIndex) this.index = NOTSET; } else { this.content = item; } } public boolean notSet() { // notSet is true if content present but nonsensical return index == NOTSET && subVals == null; } public String get(String key) { return subVals == null ? null : subVals.get(key); } public boolean has(String key) { return subVals == null ? false : subVals.containsKey(key); } public int getIndex() { return index; } public String getContent() { return content; } } /** * Helper class to allow easy extraction of information about specific * argument values (without having to check for null etc all the time) */ protected static class ArgValuesMap { protected Map m; protected ArgValuesMap() { this.newMap(); } protected ArgValuesMap(Map map) { this.m = map; } private Map getMap() { return m; } private void newMap() { m = new HashMap(); } private void newArg(Arg a) { if (m == null) newMap(); if (!containsArg(a)) m.put(a, new ArgValues(a)); } protected void addArgValue(Arg a, ArgValue av) { if (getMap() == null) m = new HashMap(); if (!m.containsKey(a)) m.put(a, new ArgValues(a)); ArgValues avs = m.get(a); avs.addArgValue(av); } protected ArgValues getArgValues(Arg a) { return m == null ? null : m.get(a); } protected ArgValues getOrCreateArgValues(Arg a) { ArgValues avs = m.get(a); if (avs == null) newArg(a); return getArgValues(a); } protected List getArgValueList(Arg a) { ArgValues avs = getArgValues(a); return avs == null ? new ArrayList<>() : avs.getArgValueList(); } protected ArgValue getArgValue(Arg a) { List vals = getArgValueList(a); return (vals == null || vals.size() == 0) ? null : vals.get(0); } protected String getValue(Arg a) { ArgValue av = getArgValue(a); return av == null ? null : av.getValue(); } protected boolean containsArg(Arg a) { if (m == null || !m.containsKey(a)) return false; return a.hasOption(Opt.STRING) ? getArgValue(a) != null : this.getBoolean(a); } protected boolean hasValue(Arg a, String val) { if (m == null || !m.containsKey(a)) return false; for (ArgValue av : getArgValueList(a)) { String avVal = av.getValue(); if ((val == null && avVal == null) || (val != null && val.equals(avVal))) { return true; } } return false; } protected boolean getBoolean(Arg a) { ArgValues av = getArgValues(a); return av == null ? false : av.getBoolean(); } protected Set getArgKeys() { return m.keySet(); } protected ArgValue getClosestPreviousArgValueOfArg(ArgValue thisAv, Arg a) { ArgValue closestAv = null; int thisArgIndex = thisAv.getArgIndex(); ArgValues compareAvs = this.getArgValues(a); int closestPreviousIndex = -1; for (ArgValue av : compareAvs.getArgValueList()) { int argIndex = av.getArgIndex(); if (argIndex < thisArgIndex && argIndex > closestPreviousIndex) { closestPreviousIndex = argIndex; closestAv = av; } } return closestAv; } protected ArgValue getClosestNextArgValueOfArg(ArgValue thisAv, Arg a) { // this looks for the *next* arg that *might* be referring back to // a thisAv. Such an arg would have no subValues (if it does it should // specify an id in the subValues so wouldn't need to be guessed). ArgValue closestAv = null; int thisArgIndex = thisAv.getArgIndex(); ArgValues compareAvs = this.getArgValues(a); int closestNextIndex = Integer.MAX_VALUE; for (ArgValue av : compareAvs.getArgValueList()) { int argIndex = av.getArgIndex(); if (argIndex > thisArgIndex && argIndex < closestNextIndex) { closestNextIndex = argIndex; closestAv = av; } } return closestAv; } protected ArgValue[] getArgValuesReferringTo(String key, String value, Arg a) { // this looks for the *next* arg that *might* be referring back to // a thisAv. Such an arg would have no subValues (if it does it should // specify an id in the subValues so wouldn't need to be guessed). List avList = new ArrayList<>(); Arg[] args = a == null ? (Arg[]) this.getMap().keySet().toArray() : new Arg[] { a }; for (Arg keyArg : args) { for (ArgValue av : this.getArgValueList(keyArg)) { } } return (ArgValue[]) avList.toArray(); } protected boolean hasId(Arg a, String id) { ArgValues avs = this.getArgValues(a); return avs == null ? false : avs.hasId(id); } protected ArgValue getId(Arg a, String id) { ArgValues avs = this.getArgValues(a); return avs == null ? null : avs.getId(id); } } public static ArgParser parseArgFiles(List argFilenameGlobs) { List argFiles = new ArrayList<>(); for (String pattern : argFilenameGlobs) { // I don't think we want to dedup files, making life easier argFiles.addAll(FileUtils.getFilesFromGlob(pattern)); } return parseArgFileList(argFiles); } public static ArgParser parseArgFileList(List argFiles) { List argsList = new ArrayList<>(); for (File argFile : argFiles) { if (!argFile.exists()) { System.err.println(DOUBLEDASH + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + "=\"" + argFile.getPath() + "\": File does not exist."); System.exit(2); } try { argsList.addAll(Files.readAllLines(Paths.get(argFile.getPath()))); } catch (IOException e) { System.err.println(DOUBLEDASH + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + "=\"" + argFile.getPath() + "\": File could not be read."); System.exit(3); } } return new ArgParser(argsList); } public static class BootstrapArgs { // only need one private static Map> bootstrapArgMap = new HashMap<>(); public static BootstrapArgs getBootstrapArgs(String[] args) { List argList = new ArrayList<>(Arrays.asList(args)); return new BootstrapArgs(argList); } private BootstrapArgs(List args) { init(args); } private void init(List args) { if (args == null) return; for (int i = 0; i < args.size(); i++) { String arg = args.get(i); String argName = null; String val = null; if (arg.startsWith(ArgParser.DOUBLEDASH)) { int equalPos = arg.indexOf('='); if (equalPos > -1) { argName = arg.substring(ArgParser.DOUBLEDASH.length(), equalPos); val = arg.substring(equalPos + 1); } else { argName = arg.substring(ArgParser.DOUBLEDASH.length()); val = "true"; } Arg a = argMap.get(argName); if (a == null || !a.hasOption(Opt.BOOTSTRAP)) { // not a valid bootstrap arg continue; } if (a.hasOption(Opt.STRING)) { if (equalPos == -1) { addAll(a, ArgParser.getShellGlobbedFilenameValues(a, args, i + 1)); } else { if (a.hasOption(Opt.GLOB)) addAll(a, FileUtils.getFilenamesFromGlob(val)); else add(a, val); } } else { add(a, val); } } } } public boolean contains(Arg a) { return bootstrapArgMap.containsKey(a); } public List getList(Arg a) { return bootstrapArgMap.get(a); } private List getOrCreateList(Arg a) { List l = getList(a); if (l == null) { l = new ArrayList<>(); putList(a, l); } return l; } private void putList(Arg a, List l) { bootstrapArgMap.put(a, l); } /* * Creates a new list if not used before, * adds the value unless the existing list is non-empty * and the arg is not MULTI (so first expressed value is * retained). */ private void add(Arg a, String s) { List l = getOrCreateList(a); if (a.hasOption(Opt.MULTI) || l.size() == 0) { l.add(s); } } private void addAll(Arg a, List al) { List l = getOrCreateList(a); if (a.hasOption(Opt.MULTI)) { l.addAll(al); } } /* * Retrieves the first value even if MULTI. * A convenience for non-MULTI args. */ public String get(Arg a) { if (!bootstrapArgMap.containsKey(a)) return null; List aL = bootstrapArgMap.get(a); return (aL == null || aL.size() == 0) ? null : aL.get(0); } } }