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