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