JAL-629 Fix --headless for new args. Fix progressbar for non-headless commands. Add...
[jalview.git] / src / jalview / bin / ArgParser.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.bin;
22
23 import java.io.File;
24 import java.io.IOException;
25 import java.net.URLDecoder;
26 import java.nio.file.Files;
27 import java.nio.file.Paths;
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.Collection;
31 import java.util.Collections;
32 import java.util.EnumSet;
33 import java.util.Enumeration;
34 import java.util.HashMap;
35 import java.util.List;
36 import java.util.Locale;
37 import java.util.Map;
38 import java.util.Set;
39
40 import jalview.util.Platform;
41
42 public class ArgParser
43 {
44   private static final String NEGATESTRING = "no";
45
46   private static final String DEFAULTLINKEDID = "";
47
48   private static enum Opt
49   {
50     BOOLEAN, STRING, UNARY, MULTI, LINKED, NODUPLICATEVALUES
51   }
52
53   // These bootstrap args are simply parsed before a full parse of arguments and
54   // so are accessible at an earlier stage to (e.g.) set debug log leve, provide
55   // a props file (that might set log level), run headlessly, read an argfile
56   // instead of other args.
57   private static final Collection<Arg> bootstrapArgs = new ArrayList(
58           Arrays.asList(Arg.PROPS, Arg.DEBUG, Arg.HEADLESS, Arg.ARGFILE));
59
60   public enum Arg
61   {
62     /*
63     NOCALCULATION, NOMENUBAR, NOSTATUS, SHOWOVERVIEW, ANNOTATIONS, COLOUR,
64     FEATURES, GROOVY, GROUPS, HEADLESS, JABAWS, NOANNOTATION, NOANNOTATION2,
65     NODISPLAY, NOGUI, NONEWS, NOQUESTIONNAIRE, NOSORTBYTREE, NOUSAGESTATS,
66     OPEN, OPEN2, PROPS, QUESTIONNAIRE, SETPROP, SORTBYTREE, TREE, VDOC,
67     VSESS;
68     */
69     HELP("h"), CALCULATION, MENUBAR, STATUS, SHOWOVERVIEW, ANNOTATIONS,
70     COLOUR, FEATURES, GROOVY, GROUPS, HEADLESS, JABAWS, ANNOTATION,
71     ANNOTATION2, DISPLAY, GUI, NEWS, NOQUESTIONNAIRE, SORTBYTREE,
72     USAGESTATS, OPEN, OPEN2, PROPS, QUESTIONNAIRE, SETPROP, TREE, VDOC,
73     VSESS, OUTPUT, OUTPUTTYPE, SSANNOTATION, NOTEMPFAC, TEMPFAC,
74     TEMPFAC_LABEL, TEMPFAC_DESC, TEMPFAC_SHADING, TITLE, PAEMATRIX, WRAP,
75     NOSTRUCTURE, STRUCTURE, IMAGE, QUIT, DEBUG("d"), ARGFILE;
76
77     static
78     {
79       HELP.setOptions(Opt.UNARY);
80       CALCULATION.setOptions(true, Opt.BOOLEAN); // default "true" implies only
81                                                  // expecting "--nocalculation"
82       MENUBAR.setOptions(true, Opt.BOOLEAN);
83       STATUS.setOptions(true, Opt.BOOLEAN);
84       SHOWOVERVIEW.setOptions(Opt.UNARY, Opt.LINKED);
85       ANNOTATIONS.setOptions(Opt.STRING, Opt.LINKED);
86       COLOUR.setOptions(Opt.STRING, Opt.LINKED);
87       FEATURES.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
88       GROOVY.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
89       GROUPS.setOptions(Opt.STRING, Opt.LINKED);
90       HEADLESS.setOptions(Opt.UNARY);
91       JABAWS.setOptions(Opt.STRING);
92       ANNOTATION.setOptions(true, Opt.BOOLEAN);
93       ANNOTATION2.setOptions(true, Opt.BOOLEAN);
94       DISPLAY.setOptions(true, Opt.BOOLEAN);
95       GUI.setOptions(true, Opt.BOOLEAN);
96       NEWS.setOptions(true, Opt.BOOLEAN);
97       NOQUESTIONNAIRE.setOptions(Opt.UNARY); // unary as --questionnaire=val
98                                              // expects a string value
99       SORTBYTREE.setOptions(true, Opt.BOOLEAN);
100       USAGESTATS.setOptions(true, Opt.BOOLEAN);
101       OPEN.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
102       OPEN2.setOptions(Opt.STRING, Opt.LINKED);
103       PROPS.setOptions(Opt.STRING);
104       QUESTIONNAIRE.setOptions(Opt.STRING);
105       SETPROP.setOptions(Opt.STRING);
106       TREE.setOptions(Opt.STRING);
107
108       VDOC.setOptions(Opt.UNARY);
109       VSESS.setOptions(Opt.UNARY);
110
111       OUTPUT.setOptions(Opt.STRING, Opt.LINKED);
112       OUTPUTTYPE.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
113
114       SSANNOTATION.setOptions(Opt.BOOLEAN, Opt.LINKED);
115       NOTEMPFAC.setOptions(Opt.UNARY, Opt.LINKED);
116       TEMPFAC.setOptions(Opt.STRING, Opt.LINKED);
117       TEMPFAC_LABEL.setOptions(Opt.STRING, Opt.LINKED);
118       TEMPFAC_DESC.setOptions(Opt.STRING, Opt.LINKED);
119       TEMPFAC_SHADING.setOptions(Opt.BOOLEAN, Opt.LINKED);
120       TITLE.setOptions(Opt.STRING, Opt.LINKED);
121       PAEMATRIX.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
122       NOSTRUCTURE.setOptions(Opt.UNARY, Opt.LINKED);
123       STRUCTURE.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
124       WRAP.setOptions(Opt.BOOLEAN, Opt.LINKED);
125       IMAGE.setOptions(Opt.STRING, Opt.LINKED);
126       QUIT.setOptions(Opt.UNARY);
127       DEBUG.setOptions(Opt.BOOLEAN);
128       ARGFILE.setOptions(Opt.STRING);
129     }
130
131     private final String[] argNames;
132
133     private Opt[] argOptions;
134
135     private boolean defaultBoolValue = false;
136
137     public String toLongString()
138     {
139       StringBuilder sb = new StringBuilder();
140       sb.append("Arg: ").append(this.name());
141       for (String name : getNames())
142       {
143         sb.append(", '").append(name).append("'");
144       }
145       sb.append("\nOptions: ");
146       boolean first = true;
147       for (Opt o : argOptions)
148       {
149         if (!first)
150         {
151           sb.append(", ");
152         }
153         sb.append(o.toString());
154         first = false;
155       }
156       sb.append("\n");
157       return sb.toString();
158     }
159
160     private Arg()
161     {
162       this(new String[0]);
163     }
164
165     private Arg(String... names)
166     {
167       int length = (names == null || names.length == 0
168               || (names.length == 1 && names[0] == null)) ? 1
169                       : names.length + 1;
170       this.argNames = new String[length];
171       this.argNames[0] = this.getName();
172       if (length > 1)
173         System.arraycopy(names, 0, this.argNames, 1, names.length);
174     }
175
176     public String[] getNames()
177     {
178       return argNames;
179     }
180
181     public String getName()
182     {
183       return this.name().toLowerCase(Locale.ROOT).replace('_', '-');
184     }
185
186     @Override
187     public final String toString()
188     {
189       return getName();
190     }
191
192     public boolean hasOption(Opt o)
193     {
194       if (argOptions == null)
195         return false;
196       for (Opt option : argOptions)
197       {
198         if (o == option)
199           return true;
200       }
201       return false;
202     }
203
204     protected void setOptions(Opt... options)
205     {
206       setOptions(false, options);
207     }
208
209     protected void setOptions(boolean defaultBoolValue, Opt... options)
210     {
211       this.defaultBoolValue = defaultBoolValue;
212       argOptions = options;
213     }
214
215     protected boolean getDefaultBoolValue()
216     {
217       return defaultBoolValue;
218     }
219   }
220
221   public static class ArgValues
222   {
223     private static final String ID = "id";
224
225     private Arg arg;
226
227     private int argCount = 0;
228
229     private boolean boolValue = false;
230
231     private boolean negated = false;
232
233     private int boolIndex = -1;
234
235     private List<Integer> argsIndexes;
236
237     private List<ArgValue> argValueList;
238
239     private Map<String, ArgValue> idMap = new HashMap<>();
240
241     protected ArgValues(Arg a)
242     {
243       this.arg = a;
244       this.argValueList = new ArrayList<ArgValue>();
245       this.boolValue = arg.getDefaultBoolValue();
246     }
247
248     public Arg arg()
249     {
250       return arg;
251     }
252
253     protected int getCount()
254     {
255       return argCount;
256     }
257
258     protected void incrementCount()
259     {
260       argCount++;
261     }
262
263     protected void setNegated(boolean b)
264     {
265       this.negated = b;
266     }
267
268     protected boolean isNegated()
269     {
270       return this.negated;
271     }
272
273     protected void setBoolean(boolean b, int i)
274     {
275       this.boolValue = b;
276       this.boolIndex = i;
277     }
278
279     protected boolean getBoolean()
280     {
281       return this.boolValue;
282     }
283
284     @Override
285     public String toString()
286     {
287       if (argValueList == null)
288         return null;
289       StringBuilder sb = new StringBuilder();
290       sb.append(arg.toLongString());
291       if (arg.hasOption(Opt.BOOLEAN) || arg.hasOption(Opt.UNARY))
292         sb.append("Boolean: ").append(boolValue).append("; Default: ")
293                 .append(arg.getDefaultBoolValue()).append("; Negated: ")
294                 .append(negated).append("\n");
295       if (arg.hasOption(Opt.STRING))
296       {
297         sb.append("Values:");
298         boolean first = true;
299         for (ArgValue av : argValueList)
300         {
301           String v = av.getValue();
302           if (!first)
303             sb.append(",");
304           sb.append("\n  '");
305           sb.append(v).append("'");
306           first = false;
307         }
308         sb.append("\n");
309       }
310       sb.append("Count: ").append(argCount).append("\n");
311       return sb.toString();
312     }
313
314     protected void addValue()
315     {
316       addValue(null, -1);
317     }
318
319     protected void addValue(String val, int argIndex)
320     {
321       addArgValue(new ArgValue(val, argIndex));
322     }
323
324     protected void addArgValue(ArgValue av)
325     {
326       if ((!arg.hasOption(Opt.MULTI) && argValueList.size() > 0)
327               || (arg.hasOption(Opt.NODUPLICATEVALUES)
328                       && argValueList.contains(av.getValue())))
329         return;
330       if (argValueList == null)
331       {
332         argValueList = new ArrayList<ArgValue>();
333       }
334       SubVals sv = ArgParser.getSubVals(av.getValue());
335       if (sv.has(ID))
336       {
337         String id = sv.get(ID);
338         av.setId(id);
339         idMap.put(id, av);
340       }
341       argValueList.add(av);
342     }
343
344     protected boolean hasValue(String val)
345     {
346       return argValueList.contains(val);
347     }
348
349     protected ArgValue getArgValue()
350     {
351       if (arg.hasOption(Opt.MULTI))
352         Console.warn("Requesting single value for multi value argument");
353       return argValueList.size() > 0 ? argValueList.get(0) : null;
354     }
355
356     protected List<ArgValue> getArgValueList()
357     {
358       return argValueList;
359     }
360
361     protected boolean hasId(String id)
362     {
363       return idMap.containsKey(id);
364     }
365
366     protected ArgValue getId(String id)
367     {
368       return idMap.get(id);
369     }
370   }
371
372   // old style
373   private List<String> vargs = null;
374
375   private boolean isApplet;
376
377   // private AppletParams appletParams;
378
379   public boolean isApplet()
380   {
381     return isApplet;
382   }
383
384   public String nextValue()
385   {
386     return vargs.remove(0);
387   }
388
389   public int getSize()
390   {
391     return vargs.size();
392   }
393
394   public String getValue(String arg)
395   {
396     return getValue(arg, false);
397   }
398
399   public String getValue(String arg, boolean utf8decode)
400   {
401     int index = vargs.indexOf(arg);
402     String dc = null;
403     String ret = null;
404     if (index != -1)
405     {
406       ret = vargs.get(index + 1).toString();
407       vargs.remove(index);
408       vargs.remove(index);
409       if (utf8decode && ret != null)
410       {
411         try
412         {
413           dc = URLDecoder.decode(ret, "UTF-8");
414           ret = dc;
415         } catch (Exception e)
416         {
417           // TODO: log failure to decode
418         }
419       }
420     }
421     return ret;
422   }
423
424   /*
425   public Object getAppletValue(String key, String def, boolean asString)
426   {
427     Object value;
428     return (appletParams == null ? null
429             : (value = appletParams.get(key.toLowerCase())) == null ? def
430                     : asString ? "" + value : value);
431   }
432   */
433
434   // new style
435   private static final Map<String, Arg> argMap;
436
437   private Map<String, ArgValuesMap> linkedArgs = new HashMap<>();
438
439   private List<String> linkedOrder = null;
440
441   private List<Arg> argList;
442
443   static
444   {
445     argMap = new HashMap<>();
446     for (Arg a : EnumSet.allOf(Arg.class))
447     {
448       ARGNAME: for (String argName : a.getNames())
449       {
450         if (argMap.containsKey(argName))
451         {
452           Console.warn("Trying to add argument name multiple times: '"
453                   + argName + "'"); // RESTORE THIS WHEN MERGED
454           if (argMap.get(argName) != a)
455           {
456             Console.error(
457                     "Trying to add argument name multiple times for different Args: '"
458                             + argMap.get(argName).getName() + ":" + argName
459                             + "' and '" + a.getName() + ":" + argName
460                             + "'");
461           }
462           continue ARGNAME;
463         }
464         argMap.put(argName, a);
465       }
466     }
467   }
468
469   public ArgParser(String[] args)
470   {
471     // old style
472     vargs = new ArrayList<>();
473     isApplet = (args.length > 0 && args[0].startsWith("<applet"));
474     if (isApplet)
475     {
476       // appletParams = AppletParams.getAppletParams(args, vargs);
477     }
478     else
479     {
480       if (Platform.isJS())
481
482       {
483         isApplet = true;
484         // appletParams =
485         // AppletParams.getAppletParams(Platform.getAppletInfoAsMap(), vargs);
486       }
487       for (int i = 0; i < args.length; i++)
488       {
489         String arg = args[i].trim();
490         if (arg.charAt(0) == '-')
491         {
492           arg = arg.substring(1);
493         }
494         vargs.add(arg);
495       }
496     }
497
498     // new style
499     Enumeration<String> argE = Collections.enumeration(Arrays.asList(args));
500     int argIndex = 0;
501     while (argE.hasMoreElements())
502     {
503       String arg = argE.nextElement();
504       String argName = null;
505       String val = null;
506       String linkedId = null;
507       if (arg.startsWith("--"))
508       {
509         int equalPos = arg.indexOf('=');
510         if (equalPos > -1)
511         {
512           argName = arg.substring(2, equalPos);
513           val = arg.substring(equalPos + 1);
514         }
515         else
516         {
517           argName = arg.substring(2);
518         }
519         int idOpen = argName.indexOf('[');
520         int idClose = argName.indexOf(']');
521
522         if (idOpen > -1 && idClose == argName.length() - 1)
523         {
524           linkedId = argName.substring(idOpen + 1, idClose);
525           argName = argName.substring(0, idOpen);
526         }
527
528         Arg a = argMap.get(argName);
529         // check for boolean prepended by "no"
530         boolean negated = false;
531         if (a == null && argName.startsWith(NEGATESTRING) && argMap
532                 .containsKey(argName.substring(NEGATESTRING.length())))
533         {
534           argName = argName.substring(NEGATESTRING.length());
535           a = argMap.get(argName);
536           negated = true;
537         }
538
539         // check for config errors
540         if (a == null)
541         {
542           // arg not found
543           Console.error("Argument '" + arg + "' not recognised. Ignoring.");
544           continue;
545         }
546         if (!a.hasOption(Opt.BOOLEAN) && negated)
547         {
548           // used "no" with a non-boolean option
549           Console.error("Argument '--" + NEGATESTRING + argName
550                   + "' not a boolean option. Ignoring.");
551           continue;
552         }
553         if (!a.hasOption(Opt.STRING) && equalPos > -1)
554         {
555           // set --argname=value when arg does not accept values
556           Console.error("Argument '--" + argName
557                   + "' does not expect a value (given as '" + arg
558                   + "').  Ignoring.");
559           continue;
560         }
561         if (!a.hasOption(Opt.LINKED) && linkedId != null)
562         {
563           // set --argname[linkedId] when arg does not use linkedIds
564           Console.error("Argument '--" + argName
565                   + "' does not expect a linked id (given as '" + arg
566                   + "'). Ignoring.");
567           continue;
568         }
569
570         if (a.hasOption(Opt.STRING) && equalPos == -1)
571         {
572           // take next arg as value if required, and '=' was not found
573           if (!argE.hasMoreElements())
574           {
575             // no value to take for arg, which wants a value
576             Console.error("Argument '" + a.getName()
577                     + "' requires a value, none given. Ignoring.");
578             continue;
579           }
580           val = argE.nextElement();
581         }
582
583         // use default linkedId for linked arguments
584         if (a.hasOption(Opt.LINKED) && linkedId == null)
585           linkedId = DEFAULTLINKEDID;
586
587         if (!linkedArgs.containsKey(linkedId))
588           linkedArgs.put(linkedId, new ArgValuesMap());
589
590         ArgValuesMap avm = linkedArgs.get(linkedId);
591
592         if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val))
593         {
594           Console.error("Argument '--" + argName
595                   + "' cannot contain a duplicate value ('" + val
596                   + "'). Ignoring this and subsequent occurrences.");
597           continue;
598         }
599
600         // check for unique id
601         SubVals sv = ArgParser.getSubVals(val);
602         String id = sv.get(ArgValues.ID);
603         if (id != null && avm.hasId(a, id))
604         {
605           Console.error("Argument '--" + argName + "' has a duplicate id ('"
606                   + id + "'). Ignoring.");
607           continue;
608         }
609
610         ArgValues avs = avm.getOrCreateArgValues(a);
611         if (avs == null)
612         {
613           avs = new ArgValues(a);
614         }
615         // store appropriate value
616         if (a.hasOption(Opt.STRING))
617         {
618           avs.addValue(val, argIndex);
619         }
620         else if (a.hasOption(Opt.BOOLEAN))
621         {
622           avs.setBoolean(!negated, argIndex);
623           avs.setNegated(negated);
624         }
625         else if (a.hasOption(Opt.UNARY))
626         {
627           avs.setBoolean(true, argIndex);
628         }
629         avs.incrementCount();
630
631         // store in appropriate place
632         if (a.hasOption(Opt.LINKED))
633         {
634           // allow a default linked id for single usage
635           if (linkedId == null)
636             linkedId = DEFAULTLINKEDID;
637           // store the order of linkedIds
638           if (linkedOrder == null)
639             linkedOrder = new ArrayList<>();
640           if (!linkedOrder.contains(linkedId))
641             linkedOrder.add(linkedId);
642         }
643
644         // store arg in the list of args used
645         if (argList == null)
646           argList = new ArrayList<>();
647         if (!argList.contains(a))
648           argList.add(a);
649       }
650     }
651   }
652
653   public boolean isSet(Arg a)
654   {
655     return a.hasOption(Opt.LINKED) ? isSet("", a) : isSet(null, a);
656   }
657
658   public boolean isSet(String linkedId, Arg a)
659   {
660     ArgValuesMap avm = linkedArgs.get(linkedId);
661     return avm == null ? false : avm.containsArg(a);
662   }
663
664   public boolean getBool(Arg a)
665   {
666     if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY))
667     {
668       Console.warn("Getting boolean from non boolean Arg '" + a.getName()
669               + "'.");
670     }
671     return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
672   }
673
674   public boolean getBool(String linkedId, Arg a)
675   {
676     ArgValuesMap avm = linkedArgs.get(linkedId);
677     if (avm == null)
678       return a.getDefaultBoolValue();
679     ArgValues avs = avm.getArgValues(a);
680     return avs == null ? a.getDefaultBoolValue() : avs.getBoolean();
681   }
682
683   public List<String> linkedIds()
684   {
685     return linkedOrder;
686   }
687
688   public ArgValuesMap linkedArgs(String id)
689   {
690     return linkedArgs.get(id);
691   }
692
693   @Override
694   public String toString()
695   {
696     StringBuilder sb = new StringBuilder();
697     sb.append("UNLINKED\n");
698     sb.append(argValuesMapToString(linkedArgs.get(null)));
699     if (linkedIds() != null)
700     {
701       sb.append("LINKED\n");
702       for (String id : linkedIds())
703       {
704         // already listed these as UNLINKED args
705         if (id == null)
706           continue;
707
708         ArgValuesMap avm = linkedArgs(id);
709         sb.append("ID: '").append(id).append("'\n");
710         sb.append(argValuesMapToString(avm));
711       }
712     }
713     return sb.toString();
714   }
715
716   private static String argValuesMapToString(ArgValuesMap avm)
717   {
718     if (avm == null)
719       return null;
720     StringBuilder sb = new StringBuilder();
721     for (Arg a : avm.getArgKeys())
722     {
723       ArgValues v = avm.getArgValues(a);
724       sb.append(v.toString());
725       sb.append("\n");
726     }
727     return sb.toString();
728   }
729
730   public static SubVals getSubVals(String item)
731   {
732     return new SubVals(item);
733   }
734
735   /**
736    * A helper class to keep an index of argument position with argument values
737    */
738   public static class ArgValue
739   {
740     private int argIndex;
741
742     private String value;
743
744     private String id;
745
746     protected ArgValue(String value, int argIndex)
747     {
748       this.value = value;
749       this.argIndex = argIndex;
750     }
751
752     protected String getValue()
753     {
754       return value;
755     }
756
757     protected int getArgIndex()
758     {
759       return argIndex;
760     }
761
762     protected void setId(String i)
763     {
764       id = i;
765     }
766
767     protected String getId()
768     {
769       return id;
770     }
771   }
772
773   /**
774    * A helper class to parse a string of the possible forms "content"
775    * "[index]content", "[keyName=keyValue]content" and return the integer index,
776    * the strings keyName and keyValue, and the content after the square brackets
777    * (if present). Values not set `will be -1 or null.
778    */
779   public static class SubVals
780   {
781     private static int NOTSET = -1;
782
783     private int index = NOTSET;
784
785     private Map<String, String> subVals = null;
786
787     private static char SEPARATOR = ';';
788
789     private String content = null;
790
791     public SubVals(String item)
792     {
793       this.parseVals(item);
794     }
795
796     public void parseVals(String item)
797     {
798       if (item == null)
799         return;
800       if (item.indexOf('[') == 0 && item.indexOf(']') > 1)
801       {
802         int openBracket = item.indexOf('[');
803         int closeBracket = item.indexOf(']');
804         String subvalsString = item.substring(openBracket + 1,
805                 closeBracket);
806         this.content = item.substring(closeBracket + 1);
807         boolean setIndex = false;
808         for (String subvalString : subvalsString
809                 .split(Character.toString(SEPARATOR)))
810         {
811           int equals = subvalString.indexOf('=');
812           if (equals > -1)
813           {
814             if (subVals == null)
815               subVals = new HashMap<>();
816             subVals.put(subvalString.substring(0, equals),
817                     subvalString.substring(equals + 1));
818           }
819           else
820           {
821             try
822             {
823               this.index = Integer.parseInt(subvalString);
824               setIndex = true;
825             } catch (NumberFormatException e)
826             {
827               Console.warn("Failed to obtain subvalue or index from '"
828                       + item + "'. Setting index=0 and using content='"
829                       + content + "'.");
830             }
831           }
832         }
833         if (!setIndex)
834           this.index = NOTSET;
835       }
836       else
837       {
838         this.content = item;
839       }
840     }
841
842     public boolean notSet()
843     {
844       // notSet is true if content present but nonsensical
845       return index == NOTSET && subVals == null;
846     }
847
848     public String get(String key)
849     {
850       return subVals == null ? null : subVals.get(key);
851     }
852
853     public boolean has(String key)
854     {
855       return subVals == null ? false : subVals.containsKey(key);
856     }
857
858     public int getIndex()
859     {
860       return index;
861     }
862
863     public String getContent()
864     {
865       return content;
866     }
867   }
868
869   /**
870    * Helper class to allow easy extraction of information about specific
871    * argument values (without having to check for null etc all the time)
872    */
873   protected static class ArgValuesMap
874   {
875     protected Map<Arg, ArgValues> m;
876
877     protected ArgValuesMap()
878     {
879       this.newMap();
880     }
881
882     protected ArgValuesMap(Map<Arg, ArgValues> map)
883     {
884       this.m = map;
885     }
886
887     private Map<Arg, ArgValues> getMap()
888     {
889       return m;
890     }
891
892     private void newMap()
893     {
894       m = new HashMap<Arg, ArgValues>();
895     }
896
897     private void newArg(Arg a)
898     {
899       if (m == null)
900         newMap();
901       if (!containsArg(a))
902         m.put(a, new ArgValues(a));
903     }
904
905     protected void addArgValue(Arg a, ArgValue av)
906     {
907       if (getMap() == null)
908         m = new HashMap<Arg, ArgValues>();
909
910       if (!m.containsKey(a))
911         m.put(a, new ArgValues(a));
912       ArgValues avs = m.get(a);
913       avs.addArgValue(av);
914     }
915
916     protected ArgValues getArgValues(Arg a)
917     {
918       return m == null ? null : m.get(a);
919     }
920
921     protected ArgValues getOrCreateArgValues(Arg a)
922     {
923       ArgValues avs = m.get(a);
924       if (avs == null)
925         newArg(a);
926       return getArgValues(a);
927     }
928
929     protected List<ArgValue> getArgValueList(Arg a)
930     {
931       ArgValues avs = getArgValues(a);
932       return avs == null ? new ArrayList<>() : avs.getArgValueList();
933     }
934
935     protected ArgValue getArgValue(Arg a)
936     {
937       List<ArgValue> vals = getArgValueList(a);
938       return (vals == null || vals.size() == 0) ? null : vals.get(0);
939     }
940
941     protected String getValue(Arg a)
942     {
943       ArgValue av = getArgValue(a);
944       return av == null ? null : av.getValue();
945     }
946
947     protected boolean containsArg(Arg a)
948     {
949       if (m == null || !m.containsKey(a))
950         return false;
951       return a.hasOption(Opt.STRING) ? getArgValue(a) != null
952               : this.getBoolean(a);
953     }
954
955     protected boolean hasValue(Arg a, String val)
956     {
957       if (m == null || !m.containsKey(a))
958         return false;
959       for (ArgValue av : getArgValueList(a))
960       {
961         String avVal = av.getValue();
962         if ((val == null && avVal == null)
963                 || (val != null && val.equals(avVal)))
964         {
965           return true;
966         }
967       }
968       return false;
969     }
970
971     protected boolean getBoolean(Arg a)
972     {
973       ArgValues av = getArgValues(a);
974       return av == null ? false : av.getBoolean();
975     }
976
977     protected Set<Arg> getArgKeys()
978     {
979       return m.keySet();
980     }
981
982     protected ArgValue getClosestPreviousArgValueOfArg(ArgValue thisAv,
983             Arg a)
984     {
985       ArgValue closestAv = null;
986       int thisArgIndex = thisAv.getArgIndex();
987       ArgValues compareAvs = this.getArgValues(a);
988       int closestPreviousIndex = -1;
989       for (ArgValue av : compareAvs.getArgValueList())
990       {
991         int argIndex = av.getArgIndex();
992         if (argIndex < thisArgIndex && argIndex > closestPreviousIndex)
993         {
994           closestPreviousIndex = argIndex;
995           closestAv = av;
996         }
997       }
998       return closestAv;
999     }
1000
1001     protected ArgValue getClosestNextArgValueOfArg(ArgValue thisAv, Arg a)
1002     {
1003       // this looks for the *next* arg that *might* be referring back to
1004       // a thisAv. Such an arg would have no subValues (if it does it should
1005       // specify an id in the subValues so wouldn't need to be guessed).
1006       ArgValue closestAv = null;
1007       int thisArgIndex = thisAv.getArgIndex();
1008       ArgValues compareAvs = this.getArgValues(a);
1009       int closestNextIndex = Integer.MAX_VALUE;
1010       for (ArgValue av : compareAvs.getArgValueList())
1011       {
1012         int argIndex = av.getArgIndex();
1013         if (argIndex > thisArgIndex && argIndex < closestNextIndex)
1014         {
1015           closestNextIndex = argIndex;
1016           closestAv = av;
1017         }
1018       }
1019       return closestAv;
1020     }
1021
1022     protected ArgValue[] getArgValuesReferringTo(String key, String value,
1023             Arg a)
1024     {
1025       // this looks for the *next* arg that *might* be referring back to
1026       // a thisAv. Such an arg would have no subValues (if it does it should
1027       // specify an id in the subValues so wouldn't need to be guessed).
1028       List<ArgValue> avList = new ArrayList<>();
1029       Arg[] args = a == null ? (Arg[]) this.getMap().keySet().toArray()
1030               : new Arg[]
1031               { a };
1032       for (Arg keyArg : args)
1033       {
1034         for (ArgValue av : this.getArgValueList(keyArg))
1035         {
1036
1037         }
1038       }
1039       return (ArgValue[]) avList.toArray();
1040     }
1041
1042     protected boolean hasId(Arg a, String id)
1043     {
1044       ArgValues avs = this.getArgValues(a);
1045       return avs == null ? false : avs.hasId(id);
1046     }
1047
1048     protected ArgValue getId(Arg a, String id)
1049     {
1050       ArgValues avs = this.getArgValues(a);
1051       return avs == null ? null : avs.getId(id);
1052     }
1053   }
1054
1055   public static Map<Arg, String> bootstrapArgs(String[] args)
1056   {
1057     Map<Arg, String> bootstrapArgMap = new HashMap<>();
1058     if (args == null)
1059       return bootstrapArgMap;
1060     Enumeration<String> argE = Collections.enumeration(Arrays.asList(args));
1061     while (argE.hasMoreElements())
1062     {
1063       String arg = argE.nextElement();
1064       String argName = null;
1065       String val = null;
1066       if (arg.startsWith("--"))
1067       {
1068         int equalPos = arg.indexOf('=');
1069         if (equalPos > -1)
1070         {
1071           argName = arg.substring(2, equalPos);
1072           val = arg.substring(equalPos + 1);
1073         }
1074         else
1075         {
1076           argName = arg.substring(2);
1077         }
1078         Arg a = argMap.get(argName);
1079         if (a != null && bootstrapArgs.contains(a))
1080           bootstrapArgMap.put(a, val);
1081       }
1082     }
1083     return bootstrapArgMap;
1084   }
1085
1086   public static ArgParser parseArgFile(String argFilename)
1087   {
1088     List<String> argsList = null;
1089     File argFile = new File(argFilename);
1090     if (!argFile.exists())
1091     {
1092       System.err.println("--" + Arg.ARGFILE.name().toLowerCase(Locale.ROOT)
1093               + "=\"" + argFilename + "\": File does not exist.");
1094       System.exit(2);
1095     }
1096     try
1097     {
1098       argsList = Files.readAllLines(Paths.get(argFilename));
1099     } catch (IOException e)
1100     {
1101       System.err.println("--" + Arg.ARGFILE.name().toLowerCase(Locale.ROOT)
1102               + "=\"" + argFilename + "\": File could not be read.");
1103       System.exit(3);
1104     }
1105     return new ArgParser((String[]) argsList.toArray());
1106   }
1107 }