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