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