JAL-629 refactor ArgParser and helper classes all to jalview.bin.argparser to remove...
authorBen Soares <b.soares@dundee.ac.uk>
Thu, 23 Mar 2023 01:11:40 +0000 (01:11 +0000)
committerBen Soares <b.soares@dundee.ac.uk>
Thu, 23 Mar 2023 01:11:40 +0000 (01:11 +0000)
15 files changed:
src/jalview/bin/ArgParser.java [deleted file]
src/jalview/bin/Commands.java
src/jalview/bin/Jalview.java
src/jalview/bin/argparser/Arg.java [new file with mode: 0644]
src/jalview/bin/argparser/ArgParser.java [new file with mode: 0644]
src/jalview/bin/argparser/ArgValue.java [new file with mode: 0644]
src/jalview/bin/argparser/ArgValues.java [new file with mode: 0644]
src/jalview/bin/argparser/ArgValuesMap.java [new file with mode: 0644]
src/jalview/bin/argparser/BootstrapArgs.java [new file with mode: 0644]
src/jalview/bin/argparser/SubVals.java [new file with mode: 0644]
src/jalview/gui/Desktop.java
src/jalview/io/FileLoader.java
test/jalview/bin/ArgParserTest.java
test/jalview/bin/CommandsTest.java
test/jalview/bin/argparser/ArgsParserTest.java [moved from test/jalview/bin/ArgsParserTest.java with 98% similarity]

diff --git a/src/jalview/bin/ArgParser.java b/src/jalview/bin/ArgParser.java
deleted file mode 100644 (file)
index fca2320..0000000
+++ /dev/null
@@ -1,1354 +0,0 @@
-/*
- * 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 <http://www.gnu.org/licenses/>.
- * 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 default linked id prefix used for no id (not even square braces)
-  private static final String DEFAULTLINKEDIDPREFIX = "JALVIEW:";
-
-  // the counter added to the default linked id prefix
-  private int defaultLinkedIdCounter = 0;
-
-  // 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;
-
-  // flag to say whether {n} subtitutions in output filenames should be made.
-  // Turn on and off with --subs and --nosubs
-  private boolean substitutions = false;
-
-  private static enum Opt
-  {
-    BOOLEAN, STRING, UNARY, MULTI, LINKED, NODUPLICATEVALUES, BOOTSTRAP,
-    GLOB, NOACTION, ALLOWSUBSTITUTIONS
-  }
-
-  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, INCREMENT, NPP("n++"), SUBSTITUTIONS, NIL;
-
-    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,
-              Opt.ALLOWSUBSTITUTIONS);
-      GROOVY.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI,
-              Opt.ALLOWSUBSTITUTIONS);
-      GROUPS.setOptions(Opt.STRING, Opt.LINKED);
-      HEADLESS.setOptions(Opt.UNARY, Opt.BOOTSTRAP);
-      JABAWS.setOptions(Opt.STRING);
-      ANNOTATION.setOptions(true, Opt.BOOLEAN, Opt.LINKED);
-      ANNOTATION2.setOptions(true, Opt.BOOLEAN, Opt.LINKED);
-      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,
-              Opt.ALLOWSUBSTITUTIONS);
-      OPEN2.setOptions(Opt.STRING, Opt.LINKED, Opt.ALLOWSUBSTITUTIONS);
-      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, Opt.ALLOWSUBSTITUTIONS);
-      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,
-              Opt.ALLOWSUBSTITUTIONS);
-      NOSTRUCTURE.setOptions(Opt.UNARY, Opt.LINKED);
-      STRUCTURE.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI,
-              Opt.ALLOWSUBSTITUTIONS);
-      WRAP.setOptions(Opt.BOOLEAN, Opt.LINKED);
-      IMAGE.setOptions(Opt.STRING, Opt.LINKED, Opt.ALLOWSUBSTITUTIONS);
-      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,
-              Opt.ALLOWSUBSTITUTIONS);
-      INCREMENT.setOptions(Opt.UNARY, Opt.MULTI, Opt.NOACTION);
-      NPP.setOptions(Opt.UNARY, Opt.MULTI, Opt.NOACTION);
-      SUBSTITUTIONS.setOptions(Opt.BOOLEAN, Opt.MULTI, Opt.NOACTION);
-      NIL.setOptions(Opt.UNARY, Opt.LINKED, Opt.MULTI, Opt.NOACTION);
-      // 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<Integer> argsIndexes;
-
-    private List<ArgValue> argValueList;
-
-    private Map<String, ArgValue> idMap = new HashMap<>();
-
-    protected ArgValues(Arg a)
-    {
-      this.arg = a;
-      this.argValueList = new ArrayList<ArgValue>();
-      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<ArgValue>();
-      }
-      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<ArgValue> 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<String> 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<String, Arg> argMap;
-
-  private Map<String, ArgValuesMap> linkedArgs = new HashMap<>();
-
-  private List<String> linkedOrder = null;
-
-  private List<Arg> 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<String> args)
-  {
-    init(args);
-  }
-
-  private void init(List<String> args)
-  {
-    // Enumeration<String> 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<String> 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);
-          }
-        }
-
-        // make NOACTION adjustments
-        // default and auto counter increments
-        if (a == Arg.INCREMENT)
-        {
-          defaultLinkedIdCounter++;
-        }
-        else if (a == Arg.NPP)
-        {
-          idCounter++;
-        }
-        else if (a == Arg.SUBSTITUTIONS)
-        {
-          substitutions = !negated;
-        }
-
-        String autoCounterString = null;
-        boolean usingAutoCounterLinkedId = false;
-        String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
-                .append(Integer.toString(defaultLinkedIdCounter))
-                .toString();
-        boolean usingDefaultLinkedId = false;
-        if (a.hasOption(Opt.LINKED))
-        {
-          if (linkedId == null)
-          {
-            // use default linkedId for linked arguments
-            linkedId = defaultLinkedId;
-            usingDefaultLinkedId = true;
-            Console.debug(
-                    "Changing linkedId to '" + linkedId + "' from " + arg);
-          }
-          else if (linkedId.equals(AUTOCOUNTERLINKEDID))
-          {
-            // turn {n} to the autoCounter
-            autoCounterString = Integer.toString(idCounter);
-            linkedId = autoCounterString;
-            usingAutoCounterLinkedId = true;
-            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;
-            usingAutoCounterLinkedId = true;
-            Console.debug(
-                    "Changing linkedId to '" + linkedId + "' from " + arg);
-          }
-        }
-
-        if (!linkedArgs.containsKey(linkedId))
-          linkedArgs.put(linkedId, new ArgValuesMap());
-
-        // do not continue for NOACTION args
-        if (a.hasOption(Opt.NOACTION))
-          continue;
-
-        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(makeSubstitutions(v), argIndex++);
-            }
-          }
-          else
-          {
-            avs.addValue(makeSubstitutions(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);
-      }
-    }
-  }
-
-  private String makeSubstitutions(String val)
-  {
-    if (!this.substitutions)
-      return val;
-
-    String subvals;
-    String rest;
-    if (val.indexOf('[') == 0 && val.indexOf(']') > 1)
-    {
-      int closeBracket = val.indexOf(']');
-      if (val.length() == closeBracket)
-        return val;
-      subvals = val.substring(0, closeBracket + 1);
-      rest = val.substring(closeBracket + 1);
-    }
-    else
-    {
-      subvals = "";
-      rest = val;
-    }
-    rest.replace(AUTOCOUNTERLINKEDID, String.valueOf(idCounter));
-    rest.replace(INCREMENTAUTOCOUNTERLINKEDID, String.valueOf(++idCounter));
-    rest.replace("{}", String.valueOf(defaultLinkedIdCounter));
-
-    return new StringBuilder(subvals).append(rest).toString();
-  }
-
-  /*
-   * 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<String> {"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<String> getShellGlobbedFilenameValues(Arg a,
-          List<String> args, int i)
-  {
-    List<String> 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<String> 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<String, String> 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 = 0;
-        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<Arg, ArgValues> m;
-
-    protected ArgValuesMap()
-    {
-      this.newMap();
-    }
-
-    protected ArgValuesMap(Map<Arg, ArgValues> map)
-    {
-      this.m = map;
-    }
-
-    private Map<Arg, ArgValues> getMap()
-    {
-      return m;
-    }
-
-    private void newMap()
-    {
-      m = new HashMap<Arg, ArgValues>();
-    }
-
-    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<Arg, ArgValues>();
-
-      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<ArgValue> getArgValueList(Arg a)
-    {
-      ArgValues avs = getArgValues(a);
-      return avs == null ? new ArrayList<>() : avs.getArgValueList();
-    }
-
-    protected ArgValue getArgValue(Arg a)
-    {
-      List<ArgValue> 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<Arg> 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<ArgValue> 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<String> argFilenameGlobs)
-  {
-    List<File> 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<File> argFiles)
-  {
-    List<String> argsList = new ArrayList<>();
-    for (File argFile : argFiles)
-    {
-      if (!argFile.exists())
-      {
-        String message = DOUBLEDASH
-                + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + "=\""
-                + argFile.getPath() + "\": File does not exist.";
-        Jalview.exit(message, 2);
-      }
-      try
-      {
-        argsList.addAll(Files.readAllLines(Paths.get(argFile.getPath())));
-      } catch (IOException e)
-      {
-        String message = DOUBLEDASH
-                + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + "=\""
-                + argFile.getPath() + "\": File could not be read.";
-        Jalview.exit(message, 3);
-      }
-    }
-    return new ArgParser(argsList);
-  }
-
-  public static class BootstrapArgs
-  {
-    // only need one
-    private static Map<Arg, List<String>> bootstrapArgMap = new HashMap<>();
-
-    public static BootstrapArgs getBootstrapArgs(String[] args)
-    {
-      List<String> argList = new ArrayList<>(Arrays.asList(args));
-      return new BootstrapArgs(argList);
-    }
-
-    private BootstrapArgs(List<String> args)
-    {
-      init(args);
-    }
-
-    private void init(List<String> 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<String> getList(Arg a)
-    {
-      return bootstrapArgMap.get(a);
-    }
-
-    private List<String> getOrCreateList(Arg a)
-    {
-      List<String> l = getList(a);
-      if (l == null)
-      {
-        l = new ArrayList<>();
-        putList(a, l);
-      }
-      return l;
-    }
-
-    private void putList(Arg a, List<String> 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<String> l = getOrCreateList(a);
-      if (a.hasOption(Opt.MULTI) || l.size() == 0)
-      {
-        l.add(s);
-      }
-    }
-
-    private void addAll(Arg a, List<String> al)
-    {
-      List<String> 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<String> aL = bootstrapArgMap.get(a);
-      return (aL == null || aL.size() == 0) ? null : aL.get(0);
-    }
-  }
-}
\ No newline at end of file
index f71ebf3..24a3c22 100644 (file)
@@ -13,10 +13,11 @@ import java.util.Map;
 
 import jalview.analysis.AlignmentUtils;
 import jalview.api.AlignmentViewPanel;
-import jalview.bin.ArgParser.Arg;
-import jalview.bin.ArgParser.ArgValue;
-import jalview.bin.ArgParser.ArgValuesMap;
-import jalview.bin.ArgParser.SubVals;
+import jalview.bin.argparser.Arg;
+import jalview.bin.argparser.ArgParser;
+import jalview.bin.argparser.ArgValue;
+import jalview.bin.argparser.ArgValuesMap;
+import jalview.bin.argparser.SubVals;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.PDBEntry;
index 046ad90..9e488dc 100755 (executable)
@@ -64,8 +64,9 @@ import com.threerings.getdown.util.LaunchUtil;
 //import edu.stanford.ejalbert.launching.IBrowserLaunching;
 import groovy.lang.Binding;
 import groovy.util.GroovyScriptEngine;
-import jalview.bin.ArgParser.Arg;
-import jalview.bin.ArgParser.BootstrapArgs;
+import jalview.bin.argparser.Arg;
+import jalview.bin.argparser.ArgParser;
+import jalview.bin.argparser.BootstrapArgs;
 import jalview.ext.so.SequenceOntology;
 import jalview.gui.AlignFrame;
 import jalview.gui.Desktop;
@@ -415,9 +416,9 @@ public class Jalview
     String usrPropsFile = bootstrapArgs.contains(Arg.PROPS)
             ? bootstrapArgs.get(Arg.PROPS)
             : aparser.getValue("props");
-    Cache.loadProperties(usrPropsFile);
     if (usrPropsFile != null)
     {
+      Cache.loadProperties(usrPropsFile);
       System.out.println(
               "CMD [-props " + usrPropsFile + "] executed successfully!");
     }
@@ -1599,7 +1600,6 @@ public class Jalview
 
   public static void exit(String message, int exitcode)
   {
-    System.err.println("####### EXITING HERE!");
     Console.debug("Using Jalview.exit");
     if (message != null)
       if (exitcode == 0)
diff --git a/src/jalview/bin/argparser/Arg.java b/src/jalview/bin/argparser/Arg.java
new file mode 100644 (file)
index 0000000..092f5d2
--- /dev/null
@@ -0,0 +1,180 @@
+package jalview.bin.argparser;
+
+import java.util.Locale;
+
+public enum Arg
+{
+  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, INCREMENT, NPP("n++"),
+  SUBSTITUTIONS, NIL;
+
+  protected static enum Opt
+  {
+    BOOLEAN, STRING, UNARY, MULTI, LINKED, NODUPLICATEVALUES, BOOTSTRAP,
+    GLOB, NOACTION, ALLOWSUBSTITUTIONS
+  }
+
+  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,
+            Opt.ALLOWSUBSTITUTIONS);
+    GROOVY.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI,
+            Opt.ALLOWSUBSTITUTIONS);
+    GROUPS.setOptions(Opt.STRING, Opt.LINKED);
+    HEADLESS.setOptions(Opt.UNARY, Opt.BOOTSTRAP);
+    JABAWS.setOptions(Opt.STRING);
+    ANNOTATION.setOptions(true, Opt.BOOLEAN, Opt.LINKED);
+    ANNOTATION2.setOptions(true, Opt.BOOLEAN, Opt.LINKED);
+    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,
+            Opt.ALLOWSUBSTITUTIONS);
+    OPEN2.setOptions(Opt.STRING, Opt.LINKED, Opt.ALLOWSUBSTITUTIONS);
+    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, Opt.ALLOWSUBSTITUTIONS);
+    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,
+            Opt.ALLOWSUBSTITUTIONS);
+    NOSTRUCTURE.setOptions(Opt.UNARY, Opt.LINKED);
+    STRUCTURE.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI,
+            Opt.ALLOWSUBSTITUTIONS);
+    WRAP.setOptions(Opt.BOOLEAN, Opt.LINKED);
+    IMAGE.setOptions(Opt.STRING, Opt.LINKED, Opt.ALLOWSUBSTITUTIONS);
+    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,
+            Opt.ALLOWSUBSTITUTIONS);
+    INCREMENT.setOptions(Opt.UNARY, Opt.MULTI, Opt.NOACTION);
+    NPP.setOptions(Opt.UNARY, Opt.MULTI, Opt.NOACTION);
+    SUBSTITUTIONS.setOptions(Opt.BOOLEAN, Opt.MULTI, Opt.NOACTION);
+    NIL.setOptions(Opt.UNARY, Opt.LINKED, Opt.MULTI, Opt.NOACTION);
+    // 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;
+  }
+}
diff --git a/src/jalview/bin/argparser/ArgParser.java b/src/jalview/bin/argparser/ArgParser.java
new file mode 100644 (file)
index 0000000..82b1b92
--- /dev/null
@@ -0,0 +1,514 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.bin.argparser;
+
+import java.io.File;
+import java.io.IOException;
+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 jalview.bin.Console;
+import jalview.bin.Jalview;
+import jalview.bin.argparser.Arg.Opt;
+import jalview.util.FileUtils;
+
+public class ArgParser
+{
+  protected static final String DOUBLEDASH = "--";
+
+  protected static final String NEGATESTRING = "no";
+
+  // the default linked id prefix used for no id (not even square braces)
+  protected static final String DEFAULTLINKEDIDPREFIX = "JALVIEW:";
+
+  // the counter added to the default linked id prefix
+  private int defaultLinkedIdCounter = 0;
+
+  // 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;
+
+  // flag to say whether {n} subtitutions in output filenames should be made.
+  // Turn on and off with --subs and --nosubs
+  private boolean substitutions = false;
+
+  protected static final Map<String, Arg> argMap;
+
+  protected Map<String, ArgValuesMap> linkedArgs = new HashMap<>();
+
+  protected List<String> linkedOrder = null;
+
+  protected List<Arg> 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<String> args)
+  {
+    init(args);
+  }
+
+  private void init(List<String> args)
+  {
+    // Enumeration<String> 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<String> 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);
+          }
+        }
+
+        // make NOACTION adjustments
+        // default and auto counter increments
+        if (a == Arg.INCREMENT)
+        {
+          defaultLinkedIdCounter++;
+        }
+        else if (a == Arg.NPP)
+        {
+          idCounter++;
+        }
+        else if (a == Arg.SUBSTITUTIONS)
+        {
+          substitutions = !negated;
+        }
+
+        String autoCounterString = null;
+        boolean usingAutoCounterLinkedId = false;
+        String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
+                .append(Integer.toString(defaultLinkedIdCounter))
+                .toString();
+        boolean usingDefaultLinkedId = false;
+        if (a.hasOption(Opt.LINKED))
+        {
+          if (linkedId == null)
+          {
+            // use default linkedId for linked arguments
+            linkedId = defaultLinkedId;
+            usingDefaultLinkedId = true;
+            Console.debug(
+                    "Changing linkedId to '" + linkedId + "' from " + arg);
+          }
+          else if (linkedId.equals(AUTOCOUNTERLINKEDID))
+          {
+            // turn {n} to the autoCounter
+            autoCounterString = Integer.toString(idCounter);
+            linkedId = autoCounterString;
+            usingAutoCounterLinkedId = true;
+            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;
+            usingAutoCounterLinkedId = true;
+            Console.debug(
+                    "Changing linkedId to '" + linkedId + "' from " + arg);
+          }
+        }
+
+        if (!linkedArgs.containsKey(linkedId))
+          linkedArgs.put(linkedId, new ArgValuesMap());
+
+        // do not continue for NOACTION args
+        if (a.hasOption(Opt.NOACTION))
+          continue;
+
+        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(makeSubstitutions(v), argIndex++);
+            }
+          }
+          else
+          {
+            avs.addValue(makeSubstitutions(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);
+      }
+    }
+  }
+
+  private String makeSubstitutions(String val)
+  {
+    if (!this.substitutions)
+      return val;
+
+    String subvals;
+    String rest;
+    if (val.indexOf('[') == 0 && val.indexOf(']') > 1)
+    {
+      int closeBracket = val.indexOf(']');
+      if (val.length() == closeBracket)
+        return val;
+      subvals = val.substring(0, closeBracket + 1);
+      rest = val.substring(closeBracket + 1);
+    }
+    else
+    {
+      subvals = "";
+      rest = val;
+    }
+    rest.replace(AUTOCOUNTERLINKEDID, String.valueOf(idCounter));
+    rest.replace(INCREMENTAUTOCOUNTERLINKEDID, String.valueOf(++idCounter));
+    rest.replace("{}", String.valueOf(defaultLinkedIdCounter));
+
+    return new StringBuilder(subvals).append(rest).toString();
+  }
+
+  /*
+   * 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<String> {"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.
+   */
+  protected static List<String> getShellGlobbedFilenameValues(Arg a,
+          List<String> args, int i)
+  {
+    List<String> 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<String> 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);
+  }
+
+  public static ArgParser parseArgFiles(List<String> argFilenameGlobs)
+  {
+    List<File> 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<File> argFiles)
+  {
+    List<String> argsList = new ArrayList<>();
+    for (File argFile : argFiles)
+    {
+      if (!argFile.exists())
+      {
+        String message = DOUBLEDASH
+                + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + "=\""
+                + argFile.getPath() + "\": File does not exist.";
+        Jalview.exit(message, 2);
+      }
+      try
+      {
+        argsList.addAll(Files.readAllLines(Paths.get(argFile.getPath())));
+      } catch (IOException e)
+      {
+        String message = DOUBLEDASH
+                + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + "=\""
+                + argFile.getPath() + "\": File could not be read.";
+        Jalview.exit(message, 3);
+      }
+    }
+    return new ArgParser(argsList);
+  }
+
+}
\ No newline at end of file
diff --git a/src/jalview/bin/argparser/ArgValue.java b/src/jalview/bin/argparser/ArgValue.java
new file mode 100644 (file)
index 0000000..be6227d
--- /dev/null
@@ -0,0 +1,39 @@
+package jalview.bin.argparser;
+
+/**
+ * A helper class to keep an index of argument position with argument values
+ */
+public class ArgValue
+{
+  private int argIndex;
+
+  private String value;
+
+  private String id;
+
+  protected ArgValue(String value, int argIndex)
+  {
+    this.value = value;
+    this.argIndex = argIndex;
+  }
+
+  public String getValue()
+  {
+    return value;
+  }
+
+  public int getArgIndex()
+  {
+    return argIndex;
+  }
+
+  protected void setId(String i)
+  {
+    id = i;
+  }
+
+  public String getId()
+  {
+    return id;
+  }
+}
\ No newline at end of file
diff --git a/src/jalview/bin/argparser/ArgValues.java b/src/jalview/bin/argparser/ArgValues.java
new file mode 100644 (file)
index 0000000..0be7768
--- /dev/null
@@ -0,0 +1,160 @@
+package jalview.bin.argparser;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import jalview.bin.Console;
+import jalview.bin.argparser.Arg.Opt;
+
+public class ArgValues
+{
+  protected 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<Integer> argsIndexes;
+
+  private List<ArgValue> argValueList;
+
+  private Map<String, ArgValue> idMap = new HashMap<>();
+
+  protected ArgValues(Arg a)
+  {
+    this.arg = a;
+    this.argValueList = new ArrayList<ArgValue>();
+    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<ArgValue>();
+    }
+    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<ArgValue> getArgValueList()
+  {
+    return argValueList;
+  }
+
+  protected boolean hasId(String id)
+  {
+    return idMap.containsKey(id);
+  }
+
+  protected ArgValue getId(String id)
+  {
+    return idMap.get(id);
+  }
+}
\ No newline at end of file
diff --git a/src/jalview/bin/argparser/ArgValuesMap.java b/src/jalview/bin/argparser/ArgValuesMap.java
new file mode 100644 (file)
index 0000000..22d85a8
--- /dev/null
@@ -0,0 +1,193 @@
+package jalview.bin.argparser;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import jalview.bin.argparser.Arg.Opt;
+
+/**
+ * Helper class to allow easy extraction of information about specific argument
+ * values (without having to check for null etc all the time)
+ */
+public class ArgValuesMap
+{
+  protected Map<Arg, ArgValues> m;
+
+  protected ArgValuesMap()
+  {
+    this.newMap();
+  }
+
+  protected ArgValuesMap(Map<Arg, ArgValues> map)
+  {
+    this.m = map;
+  }
+
+  private Map<Arg, ArgValues> getMap()
+  {
+    return m;
+  }
+
+  private void newMap()
+  {
+    m = new HashMap<Arg, ArgValues>();
+  }
+
+  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<Arg, ArgValues>();
+
+    if (!m.containsKey(a))
+      m.put(a, new ArgValues(a));
+    ArgValues avs = m.get(a);
+    avs.addArgValue(av);
+  }
+
+  public ArgValues getArgValues(Arg a)
+  {
+    return m == null ? null : m.get(a);
+  }
+
+  public ArgValues getOrCreateArgValues(Arg a)
+  {
+    ArgValues avs = m.get(a);
+    if (avs == null)
+      newArg(a);
+    return getArgValues(a);
+  }
+
+  public List<ArgValue> getArgValueList(Arg a)
+  {
+    ArgValues avs = getArgValues(a);
+    return avs == null ? new ArrayList<>() : avs.getArgValueList();
+  }
+
+  public ArgValue getArgValue(Arg a)
+  {
+    List<ArgValue> vals = getArgValueList(a);
+    return (vals == null || vals.size() == 0) ? null : vals.get(0);
+  }
+
+  public String getValue(Arg a)
+  {
+    ArgValue av = getArgValue(a);
+    return av == null ? null : av.getValue();
+  }
+
+  public boolean containsArg(Arg a)
+  {
+    if (m == null || !m.containsKey(a))
+      return false;
+    return a.hasOption(Opt.STRING) ? getArgValue(a) != null
+            : this.getBoolean(a);
+  }
+
+  public 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;
+  }
+
+  public boolean getBoolean(Arg a)
+  {
+    ArgValues av = getArgValues(a);
+    return av == null ? false : av.getBoolean();
+  }
+
+  public Set<Arg> getArgKeys()
+  {
+    return m.keySet();
+  }
+
+  public 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;
+  }
+
+  public 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;
+  }
+
+  public 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<ArgValue> 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();
+  }
+
+  public boolean hasId(Arg a, String id)
+  {
+    ArgValues avs = this.getArgValues(a);
+    return avs == null ? false : avs.hasId(id);
+  }
+
+  public ArgValue getId(Arg a, String id)
+  {
+    ArgValues avs = this.getArgValues(a);
+    return avs == null ? null : avs.getId(id);
+  }
+}
diff --git a/src/jalview/bin/argparser/BootstrapArgs.java b/src/jalview/bin/argparser/BootstrapArgs.java
new file mode 100644 (file)
index 0000000..bcb86f9
--- /dev/null
@@ -0,0 +1,143 @@
+package jalview.bin.argparser;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import jalview.bin.argparser.Arg.Opt;
+import jalview.util.FileUtils;
+
+public class BootstrapArgs
+{
+  // only need one
+  private Map<Arg, List<String>> bootstrapArgMap = new HashMap<>();
+
+  public static BootstrapArgs getBootstrapArgs(String[] args)
+  {
+    List<String> argList = new ArrayList<>(Arrays.asList(args));
+    return new BootstrapArgs(argList);
+  }
+
+  private BootstrapArgs(List<String> args)
+  {
+    init(args);
+  }
+
+  private void init(List<String> 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 = ArgParser.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<String> getList(Arg a)
+  {
+    return bootstrapArgMap.get(a);
+  }
+
+  private List<String> getOrCreateList(Arg a)
+  {
+    List<String> l = getList(a);
+    if (l == null)
+    {
+      l = new ArrayList<>();
+      putList(a, l);
+    }
+    return l;
+  }
+
+  private void putList(Arg a, List<String> 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<String> l = getOrCreateList(a);
+    if (a.hasOption(Opt.MULTI) || l.size() == 0)
+    {
+      l.add(s);
+    }
+  }
+
+  private void addAll(Arg a, List<String> al)
+  {
+    List<String> 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<String> aL = bootstrapArgMap.get(a);
+    return (aL == null || aL.size() == 0) ? null : aL.get(0);
+  }
+}
diff --git a/src/jalview/bin/argparser/SubVals.java b/src/jalview/bin/argparser/SubVals.java
new file mode 100644 (file)
index 0000000..39b48d3
--- /dev/null
@@ -0,0 +1,101 @@
+package jalview.bin.argparser;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import jalview.bin.Console;
+
+/**
+ * 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 class SubVals
+{
+  private static int NOTSET = -1;
+
+  private int index = NOTSET;
+
+  private Map<String, String> 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 = 0;
+      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;
+  }
+}
\ No newline at end of file
index 981a060..20543af 100644 (file)
@@ -2033,6 +2033,9 @@ public class Desktop extends jalview.jbgui.GDesktop
     }
     for (AlignFrame af : frames)
     {
+      System.out.println("###### frames=" + frames);
+      System.out.println("###### af=" + af);
+      System.out.println("###### af.alignPanels=" + af.alignPanels);
       for (AlignmentPanel ap : af.alignPanels)
       {
         if (alignmentId == null
index 971aba3..3b4cc31 100755 (executable)
@@ -288,6 +288,8 @@ public class FileLoader implements Runnable
   @Override
   public void run()
   {
+    System.out.println("######## Starting FileLoader.run()");
+    System.out.println("######## loading file " + file);
     String title = protocol == DataSourceType.PASTE
             ? "Copied From Clipboard"
             : file;
index b72fcb9..4487178 100644 (file)
@@ -6,15 +6,23 @@ import java.util.List;
 import java.util.Properties;
 
 import org.testng.Assert;
+import org.testng.annotations.AfterClass;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
-import jalview.bin.ArgParser.Arg;
-import jalview.bin.ArgParser.BootstrapArgs;
+import jalview.bin.argparser.Arg;
+import jalview.bin.argparser.ArgParser;
+import jalview.bin.argparser.BootstrapArgs;
 
 @Test(singleThreaded = true)
 public class ArgParserTest
 {
+  @AfterClass(alwaysRun = true)
+  public static void resetProps()
+  {
+    Cache.applicationProperties.clear();
+    Cache.loadProperties("test/jalview/testProps.jvprops");
+  }
 
   @Test(groups = "Functional", dataProvider = "argLines")
   public void parseArgsAndSubValsTest(String commandLineArgs, Arg a,
index c5f8bff..2be8458 100644 (file)
@@ -28,6 +28,7 @@ public class CommandsTest
   @AfterClass(alwaysRun = true)
   public static void resetProps()
   {
+    Cache.applicationProperties.clear();
     Cache.loadProperties("test/jalview/testProps.jvprops");
   }
 
similarity index 98%
rename from test/jalview/bin/ArgsParserTest.java
rename to test/jalview/bin/argparser/ArgsParserTest.java
index 23db36f..a163a49 100644 (file)
  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
  * The Jalview Authors are detailed in the 'AUTHORS' file.
  */
-package jalview.bin;
+package jalview.bin.argparser;
 
 import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertNull;
 import static org.testng.AssertJUnit.assertTrue;
 
+import jalview.bin.ArgsParser;
 import jalview.gui.JvOptionPane;
 
 import org.testng.annotations.BeforeClass;