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