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