/* * 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.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import jalview.util.Platform; public class ArgParser { private static final String NEGATESTRING = "no"; private static final String DEFAULTLINKEDID = ""; private static enum Opt { BOOLEAN, STRING, UNARY, MULTI, LINKED, NODUPLICATEVALUES } // These bootstrap args are simply parsed before a full parse of arguments and // so are accessible at an earlier stage to (e.g.) set debug log leve, provide // a props file (that might set log level), run headlessly, read an argfile // instead of other args. private static final Collection bootstrapArgs = new ArrayList( Arrays.asList(Arg.PROPS, Arg.DEBUG, Arg.HEADLESS, Arg.ARGFILE)); 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, DEBUG("d"), 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); 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); OPEN2.setOptions(Opt.STRING, Opt.LINKED); PROPS.setOptions(Opt.STRING); 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); DEBUG.setOptions(Opt.BOOLEAN); ARGFILE.setOptions(Opt.STRING); } 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; } /* public Object getAppletValue(String key, String def, boolean asString) { Object value; return (appletParams == null ? null : (value = appletParams.get(key.toLowerCase())) == null ? def : asString ? "" + value : value); } */ // 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)) { ARGNAME: 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 ARGNAME; } argMap.put(argName, a); } } } public ArgParser(String[] args) { // old style vargs = new ArrayList<>(); isApplet = (args.length > 0 && args[0].startsWith(" argE = Collections.enumeration(Arrays.asList(args)); int argIndex = 0; while (argE.hasMoreElements()) { String arg = argE.nextElement(); String argName = null; String val = null; String linkedId = null; if (arg.startsWith("--")) { int equalPos = arg.indexOf('='); if (equalPos > -1) { argName = arg.substring(2, equalPos); val = arg.substring(equalPos + 1); } else { argName = arg.substring(2); } 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()) { // no value to take for arg, which wants a value Console.error("Argument '" + a.getName() + "' requires a value, none given. Ignoring."); continue; } val = argE.nextElement(); } // use default linkedId for linked arguments if (a.hasOption(Opt.LINKED) && linkedId == null) linkedId = DEFAULTLINKEDID; if (!linkedArgs.containsKey(linkedId)) linkedArgs.put(linkedId, new ArgValuesMap()); ArgValuesMap avm = linkedArgs.get(linkedId); 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)) { 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); } } } 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 Map bootstrapArgs(String[] args) { Map bootstrapArgMap = new HashMap<>(); if (args == null) return bootstrapArgMap; Enumeration argE = Collections.enumeration(Arrays.asList(args)); while (argE.hasMoreElements()) { String arg = argE.nextElement(); String argName = null; String val = null; if (arg.startsWith("--")) { int equalPos = arg.indexOf('='); if (equalPos > -1) { argName = arg.substring(2, equalPos); val = arg.substring(equalPos + 1); } else { argName = arg.substring(2); } Arg a = argMap.get(argName); if (a != null && bootstrapArgs.contains(a)) bootstrapArgMap.put(a, val); } } return bootstrapArgMap; } public static ArgParser parseArgFile(String argFilename) { List argsList = null; File argFile = new File(argFilename); if (!argFile.exists()) { System.err.println("--" + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + "=\"" + argFilename + "\": File does not exist."); System.exit(2); } try { argsList = Files.readAllLines(Paths.get(argFilename)); } catch (IOException e) { System.err.println("--" + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + "=\"" + argFilename + "\": File could not be read."); System.exit(3); } return new ArgParser((String[]) argsList.toArray()); } }