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