JAL-629 --substitutions --nosubstitutions flags, --nil[{++n}] just in case needed
[jalview.git] / src / jalview / bin / ArgParser.java
index be2c07f..dfd3457 100644 (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;
+import jalview.util.FileUtils;
 
 public class ArgParser
 {
+  private static final String DOUBLEDASH = "--";
+
   private static final String NEGATESTRING = "no";
 
-  private static final String DEFAULTLINKEDID = "";
+  // 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, ORDERED
+    BOOLEAN, STRING, UNARY, MULTI, LINKED, NODUPLICATEVALUES, BOOTSTRAP,
+    GLOB, NOACTION
   }
 
   public enum Arg
@@ -60,7 +82,8 @@ public class ArgParser
     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");
+    NOSTRUCTURE, STRUCTURE, IMAGE, QUIT, CLOSE, DEBUG("d"), QUIET("q"),
+    ARGFILE, INCREMENT, NPP("n++"), SUBSTITUTIONS, NIL;
 
     static
     {
@@ -75,7 +98,7 @@ public class ArgParser
       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);
+      HEADLESS.setOptions(Opt.UNARY, Opt.BOOTSTRAP);
       JABAWS.setOptions(Opt.STRING);
       ANNOTATION.setOptions(true, Opt.BOOLEAN);
       ANNOTATION2.setOptions(true, Opt.BOOLEAN);
@@ -86,9 +109,9 @@ public class ArgParser
                                              // expects a string value
       SORTBYTREE.setOptions(true, Opt.BOOLEAN);
       USAGESTATS.setOptions(true, Opt.BOOLEAN);
-      OPEN.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
+      OPEN.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI, Opt.GLOB);
       OPEN2.setOptions(Opt.STRING, Opt.LINKED);
-      PROPS.setOptions(Opt.STRING);
+      PROPS.setOptions(Opt.STRING, Opt.BOOTSTRAP);
       QUESTIONNAIRE.setOptions(Opt.STRING);
       SETPROP.setOptions(Opt.STRING);
       TREE.setOptions(Opt.STRING);
@@ -112,7 +135,18 @@ public class ArgParser
       WRAP.setOptions(Opt.BOOLEAN, Opt.LINKED);
       IMAGE.setOptions(Opt.STRING, Opt.LINKED);
       QUIT.setOptions(Opt.UNARY);
-      DEBUG.setOptions(Opt.BOOLEAN);
+      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);
+      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;
@@ -207,6 +241,8 @@ public class ArgParser
 
   public static class ArgValues
   {
+    private static final String ID = "id";
+
     private Arg arg;
 
     private int argCount = 0;
@@ -215,16 +251,18 @@ public class ArgParser
 
     private boolean negated = false;
 
-    private int singleArgIndex = -1;
+    private int boolIndex = -1;
 
     private List<Integer> argsIndexes;
 
-    private List<ArgValue> argsList;
+    private List<ArgValue> argValueList;
+
+    private Map<String, ArgValue> idMap = new HashMap<>();
 
     protected ArgValues(Arg a)
     {
       this.arg = a;
-      this.argsList = new ArrayList<ArgValue>();
+      this.argValueList = new ArrayList<ArgValue>();
       this.boolValue = arg.getDefaultBoolValue();
     }
 
@@ -253,9 +291,10 @@ public class ArgParser
       return this.negated;
     }
 
-    protected void setBoolean(boolean b)
+    protected void setBoolean(boolean b, int i)
     {
       this.boolValue = b;
+      this.boolIndex = i;
     }
 
     protected boolean getBoolean()
@@ -266,7 +305,7 @@ public class ArgParser
     @Override
     public String toString()
     {
-      if (argsList == null)
+      if (argValueList == null)
         return null;
       StringBuilder sb = new StringBuilder();
       sb.append(arg.toLongString());
@@ -278,7 +317,7 @@ public class ArgParser
       {
         sb.append("Values:");
         boolean first = true;
-        for (ArgValue av : argsList)
+        for (ArgValue av : argValueList)
         {
           String v = av.getValue();
           if (!first)
@@ -300,44 +339,54 @@ public class ArgParser
 
     protected void addValue(String val, int argIndex)
     {
-      addValue(val, argIndex, false);
+      addArgValue(new ArgValue(val, argIndex));
     }
 
-    protected void addValue(String val, int argIndex, boolean noDuplicates)
+    protected void addArgValue(ArgValue av)
     {
-      if ((!arg.hasOption(Opt.MULTI) && argsList.size() > 0)
-              || (noDuplicates && argsList.contains(val)))
+      if ((!arg.hasOption(Opt.MULTI) && argValueList.size() > 0)
+              || (arg.hasOption(Opt.NODUPLICATEVALUES)
+                      && argValueList.contains(av.getValue())))
         return;
-      if (argsList == null)
+      if (argValueList == null)
       {
-        argsList = new ArrayList<ArgValue>();
+        argValueList = new ArrayList<ArgValue>();
       }
-      argsList.add(new ArgValue(val, argIndex));
+      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 argsList.contains(val);
+      return argValueList.contains(val);
     }
 
     protected ArgValue getArgValue()
     {
       if (arg.hasOption(Opt.MULTI))
         Console.warn("Requesting single value for multi value argument");
-      return argsList.size() > 0 ? argsList.get(0) : null;
+      return argValueList.size() > 0 ? argValueList.get(0) : null;
     }
 
-    /*
-    protected String getValue()
+    protected List<ArgValue> getArgValueList()
     {
-    ArgValue av = getArgValue();
-    return av == null ? null : av.getValue();
+      return argValueList;
     }
-    */
 
-    protected List<ArgValue> getArgValueList()
+    protected boolean hasId(String id)
     {
-      return argsList;
+      return idMap.containsKey(id);
+    }
+
+    protected ArgValue getId(String id)
+    {
+      return idMap.get(id);
     }
   }
 
@@ -371,7 +420,8 @@ public class ArgParser
   public String getValue(String arg, boolean utf8decode)
   {
     int index = vargs.indexOf(arg);
-    String dc = null, ret = null;
+    String dc = null;
+    String ret = null;
     if (index != -1)
     {
       ret = vargs.get(index + 1).toString();
@@ -392,20 +442,10 @@ public class ArgParser
     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<String, Arg> argMap;
 
-  private Map<String, HashMap<Arg, ArgValues>> linkedArgs = new HashMap<>();
+  private Map<String, ArgValuesMap> linkedArgs = new HashMap<>();
 
   private List<String> linkedOrder = null;
 
@@ -416,7 +456,7 @@ public class ArgParser
     argMap = new HashMap<>();
     for (Arg a : EnumSet.allOf(Arg.class))
     {
-      ARGNAME: for (String argName : a.getNames())
+      for (String argName : a.getNames())
       {
         if (argMap.containsKey(argName))
         {
@@ -430,7 +470,7 @@ public class ArgParser
                             + "' and '" + a.getName() + ":" + argName
                             + "'");
           }
-          continue ARGNAME;
+          continue;
         }
         argMap.put(argName, a);
       }
@@ -439,53 +479,39 @@ public class ArgParser
 
   public ArgParser(String[] args)
   {
-    // old style
-    vargs = new ArrayList<>();
-    isApplet = (args.length > 0 && args[0].startsWith("<applet"));
-    if (isApplet)
-    {
-      // appletParams = AppletParams.getAppletParams(args, vargs);
-    }
-    else
-    {
-      if (Platform.isJS())
+    // make a mutable new ArrayList so that shell globbing parser works
+    this(new ArrayList<>(Arrays.asList(args)));
+  }
 
-      {
-        isApplet = true;
-        // appletParams =
-        // AppletParams.getAppletParams(Platform.getAppletInfoAsMap(), vargs);
-      }
-      for (int i = 0; i < args.length; i++)
-      {
-        String arg = args[i].trim();
-        if (arg.charAt(0) == '-')
-        {
-          arg = arg.substring(1);
-        }
-        vargs.add(arg);
-      }
-    }
+  public ArgParser(List<String> args)
+  {
+    init(args);
+  }
 
-    // new style
-    Enumeration<String> argE = Collections.enumeration(Arrays.asList(args));
+  private void init(List<String> args)
+  {
+    // Enumeration<String> argE = Collections.enumeration(args);
     int argIndex = 0;
-    while (argE.hasMoreElements())
+    // while (argE.hasMoreElements())
+    for (int i = 0; i < args.size(); i++)
     {
-      String arg = argE.nextElement();
+      // 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("--"))
+      if (arg.startsWith(DOUBLEDASH))
       {
         int equalPos = arg.indexOf('=');
         if (equalPos > -1)
         {
-          argName = arg.substring(2, equalPos);
+          argName = arg.substring(DOUBLEDASH.length(), equalPos);
           val = arg.substring(equalPos + 1);
         }
         else
         {
-          argName = arg.substring(2);
+          argName = arg.substring(DOUBLEDASH.length());
         }
         int idOpen = argName.indexOf('[');
         int idClose = argName.indexOf(']');
@@ -541,64 +567,150 @@ public class ArgParser
         if (a.hasOption(Opt.STRING) && equalPos == -1)
         {
           // take next arg as value if required, and '=' was not found
-          if (!argE.hasMoreElements())
+          // 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;
           }
-          val = argE.nextElement();
+          // 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;
         }
 
-        // use default linkedId for linked arguments
-        if (a.hasOption(Opt.LINKED) && linkedId == null)
-          linkedId = DEFAULTLINKEDID;
+        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 HashMap<>());
+          linkedArgs.put(linkedId, new ArgValuesMap());
 
-        Map<Arg, ArgValues> valuesMap = linkedArgs.get(linkedId);
-        if (!valuesMap.containsKey(a))
-          valuesMap.put(a, new ArgValues(a));
+        // do not continue for NOACTION args
+        if (a.hasOption(Opt.NOACTION))
+          continue;
 
-        ArgValues values = valuesMap.get(a);
-        if (values == null)
+        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))
         {
-          values = new ArgValues(a);
+          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))
         {
-          values.addValue(val, argIndex);
+          if (a.hasOption(Opt.GLOB) && vals != null && vals.size() > 0)
+          {
+            for (String v : vals)
+              avs.addValue(val, argIndex++);
+          }
+          else
+          {
+            avs.addValue(val, argIndex);
+          }
         }
         else if (a.hasOption(Opt.BOOLEAN))
         {
-          values.setBoolean(!negated);
-          values.setNegated(negated);
+          avs.setBoolean(!negated, argIndex);
+          avs.setNegated(negated);
         }
         else if (a.hasOption(Opt.UNARY))
         {
-          values.setBoolean(true);
+          avs.setBoolean(true, argIndex);
         }
-        values.incrementCount();
+        avs.incrementCount();
 
         // store in appropriate place
         if (a.hasOption(Opt.LINKED))
         {
           // allow a default linked id for single usage
           if (linkedId == null)
-            linkedId = DEFAULTLINKEDID;
+            linkedId = defaultLinkedId;
           // store the order of linkedIds
           if (linkedOrder == null)
             linkedOrder = new ArrayList<>();
           if (!linkedOrder.contains(linkedId))
             linkedOrder.add(linkedId);
         }
-        // store the ArgValues
-        valuesMap.put(a, values);
 
-        // store arg in the list of args
+        // store arg in the list of args used
         if (argList == null)
           argList = new ArrayList<>();
         if (!argList.contains(a))
@@ -607,6 +719,29 @@ public class ArgParser
     }
   }
 
+  /*
+   * 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);
@@ -614,8 +749,8 @@ public class ArgParser
 
   public boolean isSet(String linkedId, Arg a)
   {
-    Map<Arg, ArgValues> m = linkedArgs.get(linkedId);
-    return m == null ? false : m.containsKey(a);
+    ArgValuesMap avm = linkedArgs.get(linkedId);
+    return avm == null ? false : avm.containsArg(a);
   }
 
   public boolean getBool(Arg a)
@@ -630,11 +765,11 @@ public class ArgParser
 
   public boolean getBool(String linkedId, Arg a)
   {
-    Map<Arg, ArgValues> m = linkedArgs.get(linkedId);
-    if (m == null)
+    ArgValuesMap avm = linkedArgs.get(linkedId);
+    if (avm == null)
       return a.getDefaultBoolValue();
-    ArgValues v = m.get(a);
-    return v == null ? a.getDefaultBoolValue() : v.getBoolean();
+    ArgValues avs = avm.getArgValues(a);
+    return avs == null ? a.getDefaultBoolValue() : avs.getBoolean();
   }
 
   public List<String> linkedIds()
@@ -642,7 +777,7 @@ public class ArgParser
     return linkedOrder;
   }
 
-  public HashMap<Arg, ArgValues> linkedArgs(String id)
+  public ArgValuesMap linkedArgs(String id)
   {
     return linkedArgs.get(id);
   }
@@ -652,7 +787,7 @@ public class ArgParser
   {
     StringBuilder sb = new StringBuilder();
     sb.append("UNLINKED\n");
-    sb.append(argMapToString(linkedArgs.get(null)));
+    sb.append(argValuesMapToString(linkedArgs.get(null)));
     if (linkedIds() != null)
     {
       sb.append("LINKED\n");
@@ -662,22 +797,22 @@ public class ArgParser
         if (id == null)
           continue;
 
-        Map<Arg, ArgValues> m = linkedArgs(id);
+        ArgValuesMap avm = linkedArgs(id);
         sb.append("ID: '").append(id).append("'\n");
-        sb.append(argMapToString(m));
+        sb.append(argValuesMapToString(avm));
       }
     }
     return sb.toString();
   }
 
-  private static String argMapToString(Map<Arg, ArgValues> m)
+  private static String argValuesMapToString(ArgValuesMap avm)
   {
-    if (m == null)
+    if (avm == null)
       return null;
     StringBuilder sb = new StringBuilder();
-    for (Arg a : m.keySet())
+    for (Arg a : avm.getArgKeys())
     {
-      ArgValues v = m.get(a);
+      ArgValues v = avm.getArgValues(a);
       sb.append(v.toString());
       sb.append("\n");
     }
@@ -698,6 +833,8 @@ public class ArgParser
 
     private String value;
 
+    private String id;
+
     protected ArgValue(String value, int argIndex)
     {
       this.value = value;
@@ -713,6 +850,16 @@ public class ArgParser
     {
       return argIndex;
     }
+
+    protected void setId(String i)
+    {
+      id = i;
+    }
+
+    protected String getId()
+    {
+      return id;
+    }
   }
 
   /**
@@ -740,6 +887,8 @@ public class ArgParser
 
     public void parseVals(String item)
     {
+      if (item == null)
+        return;
       if (item.indexOf('[') == 0 && item.indexOf(']') > 1)
       {
         int openBracket = item.indexOf('[');
@@ -817,20 +966,62 @@ public class ArgParser
   {
     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 av = getArgValues(a);
-      return av == null ? null : av.getArgValueList();
+      ArgValues avs = getArgValues(a);
+      return avs == null ? new ArrayList<>() : avs.getArgValueList();
     }
 
     protected ArgValue getArgValue(Arg a)
@@ -845,11 +1036,28 @@ public class ArgParser
       return av == null ? null : av.getValue();
     }
 
-    protected boolean hasValue(Arg a)
+    protected boolean containsArg(Arg a)
     {
-      if (!m.containsKey(a))
+      if (m == null || !m.containsKey(a))
         return false;
-      return getArgValue(a) != null;
+      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)
@@ -858,6 +1066,11 @@ public class ArgParser
       return av == null ? false : av.getBoolean();
     }
 
+    protected Set<Arg> getArgKeys()
+    {
+      return m.keySet();
+    }
+
     protected ArgValue getClosestPreviousArgValueOfArg(ArgValue thisAv,
             Arg a)
     {
@@ -876,39 +1089,231 @@ public class ArgParser
       }
       return closestAv;
     }
-  }
 
-  private static final Collection<Arg> bootstrapArgs = new ArrayList(
-          Arrays.asList(Arg.PROPS, Arg.DEBUG));
+    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;
+    }
 
-  public static Map<Arg, String> bootstrapArgs(String[] args)
-  {
-    Map<Arg, String> bootstrapArgMap = new HashMap<>();
-    if (args == null)
-      return bootstrapArgMap;
-    Enumeration<String> argE = Collections.enumeration(Arrays.asList(args));
-    while (argE.hasMoreElements())
+    protected ArgValue[] getArgValuesReferringTo(String key, String value,
+            Arg a)
     {
-      String arg = argE.nextElement();
-      String argName = null;
-      String val = null;
-      if (arg.startsWith("--"))
+      // 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)
       {
-        int equalPos = arg.indexOf('=');
-        if (equalPos > -1)
+        for (ArgValue av : this.getArgValueList(keyArg))
         {
-          argName = arg.substring(2, equalPos);
-          val = arg.substring(equalPos + 1);
+
         }
-        else
+      }
+      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())
+      {
+        System.err.println(DOUBLEDASH
+                + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + "=\""
+                + argFile.getPath() + "\": File does not exist.");
+        System.exit(2);
+      }
+      try
+      {
+        argsList.addAll(Files.readAllLines(Paths.get(argFile.getPath())));
+      } catch (IOException e)
+      {
+        System.err.println(DOUBLEDASH
+                + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + "=\""
+                + argFile.getPath() + "\": File could not be read.");
+        System.exit(3);
+      }
+    }
+    return new ArgParser(argsList);
+  }
+
+  public static class BootstrapArgs
+  {
+    // only need one
+    private static Map<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))
         {
-          argName = arg.substring(2);
+          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);
+          }
         }
-        Arg a = argMap.get(argName);
-        if (a != null && bootstrapArgs.contains(a))
-          bootstrapArgMap.put(a, val);
       }
     }
-    return bootstrapArgMap;
+
+    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