JAL-4281 store/restore ID width and manual adjustment flag in Jalview projects
[jalview.git] / src / jalview / bin / argparser / 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.argparser;
22
23 import java.io.File;
24 import java.io.IOException;
25 import java.nio.file.Files;
26 import java.nio.file.Paths;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.Collections;
30 import java.util.EnumSet;
31 import java.util.Enumeration;
32 import java.util.HashMap;
33 import java.util.List;
34 import java.util.Locale;
35 import java.util.Map;
36
37 import jalview.bin.Cache;
38 import jalview.bin.Console;
39 import jalview.bin.Jalview;
40 import jalview.bin.argparser.Arg.Opt;
41 import jalview.bin.argparser.Arg.Type;
42 import jalview.util.FileUtils;
43 import jalview.util.HttpUtils;
44
45 public class ArgParser
46 {
47   protected static final String SINGLEDASH = "-";
48
49   protected static final String DOUBLEDASH = "--";
50
51   public static final char EQUALS = '=';
52
53   public static final String STDOUTFILENAME = "-";
54
55   protected static final String NEGATESTRING = "no";
56
57   /**
58    * the default linked id prefix used for no id (ie when not even square braces
59    * are provided)
60    */
61   protected static final String DEFAULTLINKEDIDPREFIX = "JALVIEW:";
62
63   /**
64    * the linkedId string used to match all linkedIds seen so far
65    */
66   protected static final String MATCHALLLINKEDIDS = "*";
67
68   /**
69    * the linkedId string used to match all of the last --open'ed linkedIds
70    */
71   protected static final String MATCHOPENEDLINKEDIDS = "open*";
72
73   /**
74    * the counter added to the default linked id prefix
75    */
76   private int defaultLinkedIdCounter = 0;
77
78   /**
79    * the substitution string used to use the defaultLinkedIdCounter
80    */
81   private static final String DEFAULTLINKEDIDCOUNTER = "{}";
82
83   /**
84    * the linked id prefix used for --open files. NOW the same as DEFAULT
85    */
86   protected static final String OPENLINKEDIDPREFIX = DEFAULTLINKEDIDPREFIX;
87
88   /**
89    * the counter used for {n} substitutions
90    */
91   private int linkedIdAutoCounter = 0;
92
93   /**
94    * the linked id substitution string used to increment the idCounter (and use
95    * the incremented value)
96    */
97   private static final String INCREMENTLINKEDIDAUTOCOUNTER = "{++n}";
98
99   /**
100    * the linked id substitution string used to use the idCounter
101    */
102   private static final String LINKEDIDAUTOCOUNTER = "{n}";
103
104   /**
105    * the linked id substitution string used to use the filename extension of
106    * --append or --open
107    */
108   private static final String LINKEDIDEXTENSION = "{extension}";
109
110   /**
111    * the linked id substitution string used to use the base filename of --append
112    */
113   /** or --open */
114   private static final String LINKEDIDBASENAME = "{basename}";
115
116   /**
117    * the linked id substitution string used to use the dir path of --append or
118    * --open
119    */
120   private static final String LINKEDIDDIRNAME = "{dirname}";
121
122   /**
123    * the current argfile
124    */
125   private String argFile = null;
126
127   /**
128    * the linked id substitution string used to use the dir path of the latest
129    */
130   /** --argfile name */
131   private static final String ARGFILEBASENAME = "{argfilebasename}";
132
133   /**
134    * the linked id substitution string used to use the dir path of the latest
135    * --argfile name
136    */
137   private static final String ARGFILEDIRNAME = "{argfiledirname}";
138
139   /**
140    * flag to say whether {n} subtitutions in output filenames should be made.
141    * Turn on and off with --substitutions and --nosubstitutions Start with it on
142    */
143   private boolean substitutions = true;
144
145   /**
146    * flag to say whether the default linkedId is the current default linked id
147    *
148    * or ALL linkedIds
149    */
150   private boolean allLinkedIds = false;
151
152   /**
153    * flag to say whether the default linkedId is the current default linked id
154    * or OPENED linkedIds
155    */
156   private boolean openedLinkedIds = false;
157
158   /**
159    * flag to say whether the structure arguments should be applied to all
160    * structures with this linked id
161    */
162   private boolean allStructures = false;
163
164   protected static final Map<String, Arg> argMap;
165
166   protected Map<String, ArgValuesMap> linkedArgs = new HashMap<>();
167
168   protected List<String> linkedOrder = new ArrayList<>();
169
170   protected List<String> storedLinkedIds = new ArrayList<>();
171
172   protected List<Arg> argList = new ArrayList<>();
173
174   private static final char ARGFILECOMMENT = '#';
175
176   private int argIndex = 0;
177
178   private BootstrapArgs bootstrapArgs = null;
179
180   static
181   {
182     argMap = new HashMap<>();
183     for (Arg a : EnumSet.allOf(Arg.class))
184     {
185       for (String argName : a.getNames())
186       {
187         if (argMap.containsKey(argName))
188         {
189           Console.warn("Trying to add argument name multiple times: '"
190                   + argName + "'");
191           if (argMap.get(argName) != a)
192           {
193             Console.error(
194                     "Trying to add argument name multiple times for different Args: '"
195                             + argMap.get(argName).getName() + ":" + argName
196                             + "' and '" + a.getName() + ":" + argName
197                             + "'");
198           }
199           continue;
200         }
201         argMap.put(argName, a);
202       }
203     }
204   }
205
206   public ArgParser(String[] args)
207   {
208     this(args, false, null);
209   }
210
211   public ArgParser(String[] args, boolean initsubstitutions,
212           BootstrapArgs bsa)
213   {
214     /*
215      *  Make a mutable new ArrayList so that shell globbing parser works.
216      * (When shell file globbing is used, there are a sequence of non-Arg
217      * arguments (which are the expanded globbed filenames) that need to be
218      * consumed by the --append/--argfile/etc Arg which is most easily done
219      * by removing these filenames from the list one at a time. This can't be
220      * done with an ArrayList made with only Arrays.asList(String[] args) as
221      * that is not mutable. )
222      */
223     this(new ArrayList<>(Arrays.asList(args)), initsubstitutions, false,
224             bsa);
225   }
226
227   public ArgParser(List<String> args, boolean initsubstitutions)
228   {
229     this(args, initsubstitutions, false, null);
230   }
231
232   public ArgParser(List<String> args, boolean initsubstitutions,
233           boolean allowPrivate, BootstrapArgs bsa)
234   {
235     // do nothing if there are no "--" args and (some "-" args || >0 arg is
236     // "open")
237     boolean d = false;
238     boolean dd = false;
239     for (String arg : args)
240     {
241       if (arg.startsWith(DOUBLEDASH))
242       {
243         dd = true;
244         break;
245       }
246       else if (arg.startsWith("-") || arg.equals("open"))
247       {
248         d = true;
249       }
250     }
251     if (d && !dd)
252     {
253       // leave it to the old style -- parse an empty list
254       parse(new ArrayList<String>(), false, false);
255       return;
256     }
257     if (bsa != null)
258       this.bootstrapArgs = bsa;
259     else
260       this.bootstrapArgs = BootstrapArgs.getBootstrapArgs(args);
261     parse(args, initsubstitutions, allowPrivate);
262   }
263
264   private void parse(List<String> args, boolean initsubstitutions,
265           boolean allowPrivate)
266   {
267     this.substitutions = initsubstitutions;
268
269     /*
270      *  If the first argument does not start with "--" or "-" or is not "open",
271      *  and is a filename that exists or a URL, it is probably a file/list of
272      *  files to open so we insert an Arg.OPEN argument before it. This will
273      *  mean the list of files at the start of the arguments are all opened
274      *  separately.
275      */
276     if (args.size() > 0)
277     {
278       String arg0 = args.get(0);
279       if (arg0 != null
280               && (!arg0.startsWith(DOUBLEDASH) && !arg0.startsWith("-")
281                       && !arg0.equals("open") && (new File(arg0).exists()
282                               || HttpUtils.startsWithHttpOrHttps(arg0))))
283       {
284         // insert "--open" at the start
285         args.add(0, Arg.OPEN.argString());
286       }
287     }
288
289     for (int i = 0; i < args.size(); i++)
290     {
291       String arg = args.get(i);
292
293       // look for double-dash, e.g. --arg
294       if (arg.startsWith(DOUBLEDASH))
295       {
296         String argName = null;
297         String val = null;
298         List<String> globVals = null; // for Opt.GLOB only
299         SubVals globSubVals = null; // also for use by Opt.GLOB only
300         String linkedId = null;
301         Type type = null;
302
303         // look for equals e.g. --arg=value
304         int equalPos = arg.indexOf(EQUALS);
305         if (equalPos > -1)
306         {
307           argName = arg.substring(DOUBLEDASH.length(), equalPos);
308           val = arg.substring(equalPos + 1);
309         }
310         else
311         {
312           argName = arg.substring(DOUBLEDASH.length());
313         }
314
315         // look for linked ID e.g. --arg[linkedID]
316         int idOpen = argName.indexOf('[');
317         int idClose = argName.indexOf(']');
318         if (idOpen > -1 && idClose == argName.length() - 1)
319         {
320           linkedId = argName.substring(idOpen + 1, idClose);
321           argName = argName.substring(0, idOpen);
322         }
323
324         // look for type modification e.g. --help-opening
325         int dashPos = argName.indexOf(SINGLEDASH);
326         if (dashPos > -1)
327         {
328           String potentialArgName = argName.substring(0, dashPos);
329           Arg potentialArg = argMap.get(potentialArgName);
330           if (potentialArg != null && potentialArg.hasOption(Opt.HASTYPE))
331           {
332             String typeName = argName.substring(dashPos + 1);
333             try
334             {
335               type = Type.valueOf(typeName);
336             } catch (IllegalArgumentException e)
337             {
338               type = Type.INVALID;
339             }
340             argName = argName.substring(0, dashPos);
341           }
342         }
343
344         Arg a = argMap.get(argName);
345         // check for boolean prepended by "no" e.g. --nowrap
346         boolean negated = false;
347         if (a == null)
348         {
349           if (argName.startsWith(NEGATESTRING) && argMap
350                   .containsKey(argName.substring(NEGATESTRING.length())))
351           {
352             argName = argName.substring(NEGATESTRING.length());
353             a = argMap.get(argName);
354             negated = true;
355           }
356           else
357           {
358             // after all other args, look for Opt.PREFIXKEV args if still not
359             // found
360             for (Arg potentialArg : EnumSet.allOf(Arg.class))
361             {
362               if (potentialArg.hasOption(Opt.PREFIXKEV) && argName != null
363                       && argName.startsWith(potentialArg.getName())
364                       && equalPos > -1)
365               {
366                 val = argName.substring(potentialArg.getName().length())
367                         + EQUALS + val;
368                 argName = argName.substring(0,
369                         potentialArg.getName().length());
370                 a = potentialArg;
371                 break;
372               }
373             }
374           }
375         }
376
377         // check for config errors
378         if (a == null)
379         {
380           // arg not found
381           Console.error("Argument '" + arg + "' not recognised.  Exiting.");
382           Jalview.exit("Invalid argument used." + System.lineSeparator()
383                   + "Use" + System.lineSeparator() + "jalview "
384                   + Arg.HELP.argString() + System.lineSeparator()
385                   + "for a usage statement.", 13);
386           continue;
387         }
388         if (a.hasOption(Opt.PRIVATE) && !allowPrivate)
389         {
390           Console.error(
391                   "Argument '" + a.argString() + "' is private. Ignoring.");
392           continue;
393         }
394         if (!a.hasOption(Opt.BOOLEAN) && negated)
395         {
396           // used "no" with a non-boolean option
397           Console.error("Argument '" + DOUBLEDASH + NEGATESTRING + argName
398                   + "' not a boolean option. Ignoring.");
399           continue;
400         }
401         if (!a.hasOption(Opt.STRING) && equalPos > -1)
402         {
403           // set --argname=value when arg does not accept values
404           Console.error("Argument '" + a.argString()
405                   + "' does not expect a value (given as '" + arg
406                   + "').  Ignoring.");
407           continue;
408         }
409         if (!a.hasOption(Opt.LINKED) && linkedId != null)
410         {
411           // set --argname[linkedId] when arg does not use linkedIds
412           Console.error("Argument '" + a.argString()
413                   + "' does not expect a linked id (given as '" + arg
414                   + "'). Ignoring.");
415           continue;
416         }
417
418         // String value(s)
419         if (a.hasOption(Opt.STRING))
420         {
421           if (equalPos >= 0)
422           {
423             if (a.hasOption(Opt.GLOB))
424             {
425               // strip off and save the SubVals to be added individually later
426               globSubVals = new SubVals(val);
427               // make substitutions before looking for files
428               String fileGlob = makeSubstitutions(globSubVals.getContent(),
429                       linkedId);
430               globVals = FileUtils.getFilenamesFromGlob(fileGlob);
431             }
432             else
433             {
434               // val is already set -- will be saved in the ArgValue later in
435               // the normal way
436             }
437           }
438           else
439           {
440             // There is no "=" so value is next arg or args (possibly shell
441             // glob-expanded)
442             if (i + 1 >= args.size())
443             {
444               // no value to take for arg, which wants a value
445               Console.error("Argument '" + a.getName()
446                       + "' requires a value, none given. Ignoring.");
447               continue;
448             }
449             // deal with bash globs here (--arg val* is expanded before reaching
450             // the JVM). Note that SubVals cannot be used in this case.
451             // If using the --arg=val then the glob is preserved and Java globs
452             // will be used later. SubVals can be used.
453             if (a.hasOption(Opt.GLOB))
454             {
455               // if this is the first argument with a file list at the start of
456               // the args we add filenames from index i instead of i+1
457               globVals = getShellGlobbedFilenameValues(a, args, i + 1);
458             }
459             else
460             {
461               val = args.get(i + 1);
462             }
463           }
464         }
465
466         // make NOACTION adjustments
467         // default and auto counter increments
468         if (a == Arg.NPP)
469         {
470           linkedIdAutoCounter++;
471         }
472         else if (a == Arg.SUBSTITUTIONS)
473         {
474           substitutions = !negated;
475         }
476         else if (a == Arg.SETARGFILE)
477         {
478           argFile = val;
479         }
480         else if (a == Arg.UNSETARGFILE)
481         {
482           argFile = null;
483         }
484         else if (a == Arg.ALL)
485         {
486           allLinkedIds = !negated;
487           openedLinkedIds = false;
488         }
489         else if (a == Arg.OPENED)
490         {
491           openedLinkedIds = !negated;
492           allLinkedIds = false;
493         }
494         else if (a == Arg.ALLSTRUCTURES)
495         {
496           allStructures = !negated;
497         }
498
499         if (a.hasOption(Opt.STORED))
500         {
501           // reset the lastOpenedLinkedIds list
502           this.storedLinkedIds = new ArrayList<>();
503         }
504
505         // this is probably only Arg.NEW and Arg.OPEN
506         if (a.hasOption(Opt.INCREMENTDEFAULTCOUNTER))
507         {
508           // use the next default prefixed OPENLINKEDID
509           defaultLinkedId(true);
510         }
511
512         String autoCounterString = null;
513         String defaultLinkedId = defaultLinkedId(false);
514         boolean usingDefaultLinkedId = false;
515         if (a.hasOption(Opt.LINKED))
516         {
517           if (linkedId == null)
518           {
519             if (a.hasOption(Opt.OUTPUTFILE) && a.hasOption(Opt.ALLOWALL)
520                     && val.startsWith(MATCHALLLINKEDIDS))
521             {
522               // --output=*.ext is shorthand for --all --output {basename}.ext
523               // (or --image=*.ext)
524               allLinkedIds = true;
525               openedLinkedIds = false;
526               linkedId = MATCHALLLINKEDIDS;
527               val = LINKEDIDDIRNAME + File.separator + LINKEDIDBASENAME
528                       + val.substring(MATCHALLLINKEDIDS.length());
529             }
530             else if (a.hasOption(Opt.OUTPUTFILE)
531                     && a.hasOption(Opt.ALLOWALL)
532                     && val.startsWith(MATCHOPENEDLINKEDIDS))
533             {
534               // --output=open*.ext is shorthand for --opened --output
535               // {basename}.ext
536               // (or --image=open*.ext)
537               openedLinkedIds = true;
538               allLinkedIds = false;
539               linkedId = MATCHOPENEDLINKEDIDS;
540               val = LINKEDIDDIRNAME + File.separator + LINKEDIDBASENAME
541                       + val.substring(MATCHOPENEDLINKEDIDS.length());
542             }
543             else if (allLinkedIds && a.hasOption(Opt.ALLOWALL))
544             {
545               linkedId = MATCHALLLINKEDIDS;
546             }
547             else if (openedLinkedIds && a.hasOption(Opt.ALLOWALL))
548             {
549               linkedId = MATCHOPENEDLINKEDIDS;
550             }
551             else
552             {
553               // use default linkedId for linked arguments
554               linkedId = defaultLinkedId;
555               usingDefaultLinkedId = true;
556               Console.debug("Changing linkedId to '" + linkedId + "' from "
557                       + arg);
558             }
559           }
560           else
561           {
562             if (linkedId.contains(LINKEDIDAUTOCOUNTER))
563             {
564               // turn {n} to the autoCounter
565               autoCounterString = Integer.toString(linkedIdAutoCounter);
566               linkedId = linkedId.replace(LINKEDIDAUTOCOUNTER,
567                       autoCounterString);
568               Console.debug("Changing linkedId to '" + linkedId + "' from "
569                       + arg);
570             }
571             if (linkedId.contains(INCREMENTLINKEDIDAUTOCOUNTER))
572             {
573               // turn {++n} to the incremented autoCounter
574               autoCounterString = Integer.toString(++linkedIdAutoCounter);
575               linkedId = linkedId.replace(INCREMENTLINKEDIDAUTOCOUNTER,
576                       autoCounterString);
577               Console.debug("Changing linkedId to '" + linkedId + "' from "
578                       + arg);
579             }
580           }
581         }
582
583         // do not continue in this block for NOACTION args
584         if (a.hasOption(Opt.NOACTION))
585           continue;
586
587         ArgValuesMap avm = getOrCreateLinkedArgValuesMap(linkedId);
588
589         // not dealing with both NODUPLICATEVALUES and GLOB
590         if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val))
591         {
592           Console.error("Argument '" + a.argString()
593                   + "' cannot contain a duplicate value ('" + val
594                   + "'). Ignoring this and subsequent occurrences.");
595           continue;
596         }
597
598         // check for unique id
599         SubVals subvals = new SubVals(val);
600         boolean addNewSubVals = false;
601         String id = subvals.get(ArgValues.ID);
602         if (id != null && avm.hasId(a, id))
603         {
604           Console.error("Argument '" + a.argString()
605                   + "' has a duplicate id ('" + id + "'). Ignoring.");
606           continue;
607         }
608
609         // set allstructures to all non-primary structure options in this linked
610         // id if --allstructures has been set
611         if (allStructures
612                 && (a.getType() == Type.STRUCTURE
613                         || a.getType() == Type.STRUCTUREIMAGE)
614                 && !a.hasOption(Opt.PRIMARY))
615         {
616           if (!subvals.has(Arg.ALLSTRUCTURES.getName()))
617           // && !subvals.has("structureid"))
618           {
619             subvals.put(Arg.ALLSTRUCTURES.getName(), "true");
620             addNewSubVals = true;
621           }
622         }
623
624         ArgValues avs = avm.getOrCreateArgValues(a);
625
626         // store appropriate String value(s)
627         if (a.hasOption(Opt.STRING))
628         {
629           if (a.hasOption(Opt.GLOB) && globVals != null
630                   && globVals.size() > 0)
631           {
632             Enumeration<String> gve = Collections.enumeration(globVals);
633             while (gve.hasMoreElements())
634             {
635               String v = gve.nextElement();
636               SubVals vsv = new SubVals(globSubVals, v);
637               addValue(linkedId, type, avs, vsv, v, argIndex++, true);
638               // if we're using defaultLinkedId and the arg increments the
639               // counter:
640               if (gve.hasMoreElements() && usingDefaultLinkedId
641                       && a.hasOption(Opt.INCREMENTDEFAULTCOUNTER))
642               {
643                 // increment the default linkedId
644                 linkedId = defaultLinkedId(true);
645                 // get new avm and avs
646                 avm = linkedArgs.get(linkedId);
647                 avs = avm.getOrCreateArgValues(a);
648               }
649             }
650           }
651           else
652           {
653             // addValue(linkedId, type, avs, val, argIndex, true);
654             addValue(linkedId, type, avs, addNewSubVals ? subvals : null,
655                     val, argIndex, true);
656           }
657         }
658         else if (a.hasOption(Opt.BOOLEAN))
659         {
660           setBoolean(linkedId, type, avs, !negated, argIndex);
661           setNegated(linkedId, avs, negated);
662         }
663         else if (a.hasOption(Opt.UNARY))
664         {
665           setBoolean(linkedId, type, avs, true, argIndex);
666         }
667
668         // remove the '*' or 'open*' linkedId that should be empty if it was
669         // created
670         if ((MATCHALLLINKEDIDS.equals(linkedId)
671                 && linkedArgs.containsKey(linkedId))
672                 || (MATCHOPENEDLINKEDIDS.equals(linkedId)
673                         && linkedArgs.containsKey(linkedId)))
674         {
675           linkedArgs.remove(linkedId);
676         }
677       }
678     }
679   }
680
681   private void finaliseStoringArgValue(String linkedId, ArgValues avs)
682   {
683     Arg a = avs.arg();
684     incrementCount(linkedId, avs);
685     argIndex++;
686
687     // store in appropriate place
688     if (a.hasOption(Opt.LINKED))
689     {
690       // store the order of linkedIds
691       if (!linkedOrder.contains(linkedId))
692         linkedOrder.add(linkedId);
693     }
694
695     // store arg in the list of args used
696     if (!argList.contains(a))
697       argList.add(a);
698   }
699
700   private String defaultLinkedId(boolean increment)
701   {
702     String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
703             .append(Integer.toString(defaultLinkedIdCounter)).toString();
704     if (increment)
705     {
706       while (linkedArgs.containsKey(defaultLinkedId))
707       {
708         defaultLinkedIdCounter++;
709         defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
710                 .append(Integer.toString(defaultLinkedIdCounter))
711                 .toString();
712       }
713     }
714     getOrCreateLinkedArgValuesMap(defaultLinkedId);
715     return defaultLinkedId;
716   }
717
718   public String makeSubstitutions(String val, String linkedId)
719   {
720     if (!this.substitutions || val == null)
721       return val;
722
723     String subvals;
724     String rest;
725     if (val.indexOf('[') == 0 && val.indexOf(']') > 1)
726     {
727       int closeBracket = val.indexOf(']');
728       if (val.length() == closeBracket)
729         return val;
730       subvals = val.substring(0, closeBracket + 1);
731       rest = val.substring(closeBracket + 1);
732     }
733     else
734     {
735       subvals = "";
736       rest = val;
737     }
738     if (rest.contains(LINKEDIDAUTOCOUNTER))
739       rest = rest.replace(LINKEDIDAUTOCOUNTER,
740               String.valueOf(linkedIdAutoCounter));
741     if (rest.contains(INCREMENTLINKEDIDAUTOCOUNTER))
742       rest = rest.replace(INCREMENTLINKEDIDAUTOCOUNTER,
743               String.valueOf(++linkedIdAutoCounter));
744     if (rest.contains(DEFAULTLINKEDIDCOUNTER))
745       rest = rest.replace(DEFAULTLINKEDIDCOUNTER,
746               String.valueOf(defaultLinkedIdCounter));
747     ArgValuesMap avm = linkedArgs.get(linkedId);
748     if (avm != null)
749     {
750       if (rest.contains(LINKEDIDBASENAME))
751       {
752         rest = rest.replace(LINKEDIDBASENAME, avm.getBasename());
753       }
754       if (rest.contains(LINKEDIDEXTENSION))
755       {
756         rest = rest.replace(LINKEDIDEXTENSION, avm.getExtension());
757       }
758       if (rest.contains(LINKEDIDDIRNAME))
759       {
760         rest = rest.replace(LINKEDIDDIRNAME, avm.getDirname());
761       }
762     }
763     if (argFile != null)
764     {
765       if (rest.contains(ARGFILEBASENAME))
766       {
767         rest = rest.replace(ARGFILEBASENAME,
768                 FileUtils.getBasename(new File(argFile)));
769       }
770       if (rest.contains(ARGFILEDIRNAME))
771       {
772         rest = rest.replace(ARGFILEDIRNAME,
773                 FileUtils.getDirname(new File(argFile)));
774       }
775     }
776
777     return new StringBuilder(subvals).append(rest).toString();
778   }
779
780   /*
781    * A helper method to take a list of String args where we're expecting
782    * {"--previousargs", "--arg", "file1", "file2", "file3", "--otheroptionsornot"}
783    * and the index of the globbed arg, here 1. It returns a List<String> {"file1",
784    * "file2", "file3"} *and remove these from the original list object* so that
785    * processing can continue from where it has left off, e.g. args has become
786    * {"--previousargs", "--arg", "--otheroptionsornot"} so the next increment
787    * carries on from the next --arg if available.
788    */
789   protected static List<String> getShellGlobbedFilenameValues(Arg a,
790           List<String> args, int i)
791   {
792     List<String> vals = new ArrayList<>();
793     while (i < args.size() && !args.get(i).startsWith(DOUBLEDASH))
794     {
795       vals.add(FileUtils.substituteHomeDir(args.remove(i)));
796       if (!a.hasOption(Opt.GLOB))
797         break;
798     }
799     return vals;
800   }
801
802   public BootstrapArgs getBootstrapArgs()
803   {
804     return bootstrapArgs;
805   }
806
807   public boolean isSet(Arg a)
808   {
809     return a.hasOption(Opt.LINKED) ? isSetAtAll(a) : isSet(null, a);
810   }
811
812   public boolean isSetAtAll(Arg a)
813   {
814     for (String linkedId : linkedOrder)
815     {
816       if (isSet(linkedId, a))
817         return true;
818     }
819     return false;
820   }
821
822   public boolean isSet(String linkedId, Arg a)
823   {
824     ArgValuesMap avm = linkedArgs.get(linkedId);
825     return avm == null ? false : avm.containsArg(a);
826   }
827
828   public boolean getBoolean(Arg a)
829   {
830     if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY))
831     {
832       Console.warn("Getting boolean from non boolean Arg '" + a.getName()
833               + "'.");
834     }
835     return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
836   }
837
838   public boolean getBool(String linkedId, Arg a)
839   {
840     ArgValuesMap avm = linkedArgs.get(linkedId);
841     if (avm == null)
842       return a.getDefaultBoolValue();
843     ArgValues avs = avm.getArgValues(a);
844     return avs == null ? a.getDefaultBoolValue() : avs.getBoolean();
845   }
846
847   public List<String> getLinkedIds()
848   {
849     return linkedOrder;
850   }
851
852   public ArgValuesMap getLinkedArgs(String id)
853   {
854     return linkedArgs.get(id);
855   }
856
857   @Override
858   public String toString()
859   {
860     StringBuilder sb = new StringBuilder();
861     sb.append("UNLINKED\n");
862     sb.append(argValuesMapToString(linkedArgs.get(null)));
863     if (getLinkedIds() != null)
864     {
865       sb.append("LINKED\n");
866       for (String id : getLinkedIds())
867       {
868         // already listed these as UNLINKED args
869         if (id == null)
870           continue;
871
872         ArgValuesMap avm = getLinkedArgs(id);
873         sb.append("ID: '").append(id).append("'\n");
874         sb.append(argValuesMapToString(avm));
875       }
876     }
877     return sb.toString();
878   }
879
880   private static String argValuesMapToString(ArgValuesMap avm)
881   {
882     if (avm == null)
883       return null;
884     StringBuilder sb = new StringBuilder();
885     for (Arg a : avm.getArgKeys())
886     {
887       ArgValues v = avm.getArgValues(a);
888       sb.append(v.toString());
889       sb.append("\n");
890     }
891     return sb.toString();
892   }
893
894   public static ArgParser parseArgFiles(List<String> argFilenameGlobs,
895           boolean initsubstitutions, BootstrapArgs bsa)
896   {
897     List<File> argFiles = new ArrayList<>();
898
899     for (String pattern : argFilenameGlobs)
900     {
901       // I don't think we want to dedup files, making life easier
902       argFiles.addAll(FileUtils.getFilesFromGlob(pattern));
903     }
904
905     return parseArgFileList(argFiles, initsubstitutions, bsa);
906   }
907
908   public static ArgParser parseArgFileList(List<File> argFiles,
909           boolean initsubstitutions, BootstrapArgs bsa)
910   {
911     List<String> argsList = new ArrayList<>();
912     for (File argFile : argFiles)
913     {
914       if (!argFile.exists())
915       {
916         String message = Arg.ARGFILE.argString() + EQUALS + "\""
917                 + argFile.getPath() + "\": File does not exist.";
918         Jalview.exit(message, 2);
919       }
920       try
921       {
922         String setargfile = new StringBuilder(Arg.SETARGFILE.argString())
923                 .append(EQUALS).append(argFile.getCanonicalPath())
924                 .toString();
925         argsList.add(setargfile);
926         argsList.addAll(readArgFile(argFile));
927         argsList.add(Arg.UNSETARGFILE.argString());
928       } catch (IOException e)
929       {
930         String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
931                 + "\": File could not be read.";
932         Jalview.exit(message, 3);
933       }
934     }
935     // Third param "true" uses Opt.PRIVATE args --setargile=argfile and
936     // --unsetargfile
937     return new ArgParser(argsList, initsubstitutions, true, bsa);
938   }
939
940   protected static List<String> readArgFile(File argFile)
941   {
942     List<String> args = new ArrayList<>();
943     if (argFile != null && argFile.exists())
944     {
945       try
946       {
947         for (String line : Files.readAllLines(Paths.get(argFile.getPath())))
948         {
949           if (line != null && line.length() > 0
950                   && line.charAt(0) != ARGFILECOMMENT)
951             args.add(line);
952         }
953       } catch (IOException e)
954       {
955         String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
956                 + "\": File could not be read.";
957         Console.debug(message, e);
958         Jalview.exit(message, 3);
959       }
960     }
961     return args;
962   }
963
964   public static enum Position
965   {
966     FIRST, BEFORE, AFTER
967   }
968
969   /**
970    * get from following Arg of type a or subval of same name (lowercase)
971    */
972   public static String getValueFromSubValOrArg(ArgValuesMap avm,
973           ArgValue av, Arg a, SubVals sv)
974   {
975     return getFromSubValArgOrPref(avm, av, a, sv, null, null, null);
976   }
977
978   /**
979    * get from following Arg of type a or subval key or preference pref or
980    * default def
981    */
982   public static String getFromSubValArgOrPref(ArgValuesMap avm, ArgValue av,
983           Arg a, SubVals sv, String key, String pref, String def)
984   {
985     return getFromSubValArgOrPref(avm, a, Position.AFTER, av, sv, key, pref,
986             def);
987   }
988
989   /**
990    * get from following(AFTER), first occurence of (FIRST) or previous (BEFORE)
991    * Arg of type a or subval key or preference pref or default def
992    */
993   public static String getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
994           Position pos, ArgValue av, SubVals sv, String key, String pref,
995           String def)
996   {
997     return getFromSubValArgOrPrefWithSubstitutions(null, avm, a, pos, av,
998             sv, key, pref, def);
999   }
1000
1001   public static String getFromSubValArgOrPrefWithSubstitutions(ArgParser ap,
1002           ArgValuesMap avm, Arg a, Position pos, ArgValue av, SubVals sv,
1003           String key, String pref, String def)
1004   {
1005     if (key == null)
1006       key = a.getName();
1007     String value = null;
1008     if (sv != null && sv.has(key) && sv.get(key) != null)
1009     {
1010       value = ap == null ? sv.get(key)
1011               : sv.getWithSubstitutions(ap, avm.getLinkedId(), key);
1012     }
1013     else if (avm != null && avm.containsArg(a))
1014     {
1015       if (pos == Position.FIRST && avm.getValue(a) != null)
1016         value = avm.getValue(a);
1017       else if (pos == Position.BEFORE
1018               && avm.getClosestPreviousArgValueOfArg(av, a) != null)
1019         value = avm.getClosestPreviousArgValueOfArg(av, a).getValue();
1020       else if (pos == Position.AFTER
1021               && avm.getClosestNextArgValueOfArg(av, a) != null)
1022         value = avm.getClosestNextArgValueOfArg(av, a).getValue();
1023
1024       // look for allstructures subval for Type.STRUCTURE*
1025       Arg arg = av.getArg();
1026       if (value == null && arg.hasOption(Opt.PRIMARY)
1027               && arg.getType() == Type.STRUCTURE
1028               && !a.hasOption(Opt.PRIMARY) && (a.getType() == Type.STRUCTURE
1029                       || a.getType() == Type.STRUCTUREIMAGE))
1030       {
1031         ArgValue av2 = avm.getArgValueOfArgWithSubValKey(a,
1032                 Arg.ALLSTRUCTURES.getName());
1033         if (av2 != null)
1034         {
1035           value = av2.getValue();
1036         }
1037       }
1038     }
1039     if (value == null)
1040     {
1041       value = pref != null ? Cache.getDefault(pref, def) : def;
1042     }
1043     return value;
1044   }
1045
1046   public static boolean getBoolFromSubValOrArg(ArgValuesMap avm, Arg a,
1047           SubVals sv)
1048   {
1049     return getFromSubValArgOrPref(avm, a, sv, null, null, false);
1050   }
1051
1052   public static boolean getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
1053           SubVals sv, String key, String pref, boolean def)
1054   {
1055     return getFromSubValArgOrPref(avm, a, sv, key, pref, def, false);
1056   }
1057
1058   public static boolean getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
1059           SubVals sv, String key, String pref, boolean def,
1060           boolean invertPref)
1061   {
1062     if ((key == null && a == null) || (sv == null && a == null))
1063       return false;
1064
1065     boolean usingArgKey = false;
1066     if (key == null)
1067     {
1068       key = a.getName();
1069       usingArgKey = true;
1070     }
1071
1072     String nokey = ArgParser.NEGATESTRING + key;
1073
1074     // look for key or nokey in subvals first (if using Arg check options)
1075     if (sv != null)
1076     {
1077       // check for true boolean
1078       if (sv.has(key) && sv.get(key) != null)
1079       {
1080         if (usingArgKey)
1081         {
1082           if (!(a.hasOption(Opt.BOOLEAN) || a.hasOption(Opt.UNARY)))
1083           {
1084             Console.debug(
1085                     "Looking for boolean in subval from non-boolean/non-unary Arg "
1086                             + a.getName());
1087             return false;
1088           }
1089         }
1090         return sv.get(key).toLowerCase(Locale.ROOT).equals("true");
1091       }
1092
1093       // check for negative boolean (subval "no..." will be "true")
1094       if (sv.has(nokey) && sv.get(nokey) != null)
1095       {
1096         if (usingArgKey)
1097         {
1098           if (!(a.hasOption(Opt.BOOLEAN)))
1099           {
1100             Console.debug(
1101                     "Looking for negative boolean in subval from non-boolean Arg "
1102                             + a.getName());
1103             return false;
1104           }
1105         }
1106         return !sv.get(nokey).toLowerCase(Locale.ROOT).equals("true");
1107       }
1108     }
1109
1110     // check argvalues
1111     if (avm != null && avm.containsArg(a))
1112       return avm.getBoolean(a);
1113
1114     // return preference or default
1115     boolean prefVal = pref != null ? Cache.getDefault(pref, def) : false;
1116     return pref != null ? (invertPref ? !prefVal : prefVal) : def;
1117   }
1118
1119   // the following methods look for the "*" linkedId and add the argvalue to all
1120   // linkedId ArgValues if it does.
1121   /**
1122    * This version inserts the subvals sv into all created values
1123    */
1124   private void addValue(String linkedId, Type type, ArgValues avs,
1125           SubVals sv, String v, int argIndex, boolean doSubs)
1126   {
1127     this.argValueOperation(Op.ADDVALUE, linkedId, type, avs, sv, v, false,
1128             argIndex, doSubs);
1129   }
1130
1131   private void addValue(String linkedId, Type type, ArgValues avs, String v,
1132           int argIndex, boolean doSubs)
1133   {
1134     this.argValueOperation(Op.ADDVALUE, linkedId, type, avs, null, v, false,
1135             argIndex, doSubs);
1136   }
1137
1138   private void setBoolean(String linkedId, Type type, ArgValues avs,
1139           boolean b, int argIndex)
1140   {
1141     this.argValueOperation(Op.SETBOOLEAN, linkedId, type, avs, null, null,
1142             b, argIndex, false);
1143   }
1144
1145   private void setNegated(String linkedId, ArgValues avs, boolean b)
1146   {
1147     this.argValueOperation(Op.SETNEGATED, linkedId, null, avs, null, null,
1148             b, 0, false);
1149   }
1150
1151   private void incrementCount(String linkedId, ArgValues avs)
1152   {
1153     this.argValueOperation(Op.INCREMENTCOUNT, linkedId, null, avs, null,
1154             null, false, 0, false);
1155   }
1156
1157   private enum Op
1158   {
1159     ADDVALUE, SETBOOLEAN, SETNEGATED, INCREMENTCOUNT
1160   }
1161
1162   private void argValueOperation(Op op, String linkedId, Type type,
1163           ArgValues avs, SubVals sv, String v, boolean b, int argIndex,
1164           boolean doSubs)
1165   {
1166     // default to merge subvals if subvals are provided
1167     argValueOperation(op, linkedId, type, avs, sv, true, v, b, argIndex,
1168             doSubs);
1169   }
1170
1171   /**
1172    * The following operations look for the "*" and "open*" linkedIds and add the
1173    * argvalue to all appropriate linkedId ArgValues if it does. If subvals are
1174    * supplied, they are inserted into all new set values.
1175    * 
1176    * @param op
1177    *          The ArgParser.Op operation
1178    * @param linkedId
1179    *          The String linkedId from the ArgValuesMap
1180    * @param type
1181    *          The Arg.Type to attach to this ArgValue
1182    * @param avs
1183    *          The ArgValues for this linkedId
1184    * @param sv
1185    *          Use these SubVals on the ArgValue
1186    * @param merge
1187    *          Merge the SubVals with any existing on the value. False will
1188    *          replace unless sv is null
1189    * @param v
1190    *          The value of the ArgValue (may contain subvals).
1191    * @param b
1192    *          The boolean value of the ArgValue.
1193    * @param argIndex
1194    *          The argIndex for the ArgValue.
1195    * @param doSubs
1196    *          Whether to perform substitutions on the subvals and value.
1197    */
1198   private void argValueOperation(Op op, String linkedId, Type type,
1199           ArgValues avs, SubVals sv, boolean merge, String v, boolean b,
1200           int argIndex, boolean doSubs)
1201   {
1202     Arg a = avs.arg();
1203
1204     List<String> wildcardLinkedIds = null;
1205     if (a.hasOption(Opt.ALLOWALL))
1206     {
1207       switch (linkedId)
1208       {
1209       case MATCHALLLINKEDIDS:
1210         wildcardLinkedIds = getLinkedIds();
1211         break;
1212       case MATCHOPENEDLINKEDIDS:
1213         wildcardLinkedIds = this.storedLinkedIds;
1214         break;
1215       }
1216     }
1217
1218     // if we're not a wildcard linkedId and the arg is marked to be stored, add
1219     // to storedLinkedIds
1220     if (linkedId != null && wildcardLinkedIds == null
1221             && a.hasOption(Opt.STORED)
1222             && !storedLinkedIds.contains(linkedId))
1223     {
1224       storedLinkedIds.add(linkedId);
1225     }
1226
1227     // if we are a wildcard linkedId, apply the arg and value to all appropriate
1228     // linkedIds
1229     if (wildcardLinkedIds != null)
1230     {
1231       for (String id : wildcardLinkedIds)
1232       {
1233         // skip incorrectly stored wildcard ids!
1234         if (id == null || MATCHALLLINKEDIDS.equals(id)
1235                 || MATCHOPENEDLINKEDIDS.equals(id))
1236           continue;
1237         ArgValuesMap avm = linkedArgs.get(id);
1238         // don't set an output if there isn't an input
1239         if (a.hasOption(Opt.REQUIREINPUT)
1240                 && !avm.hasArgWithOption(Opt.INPUT))
1241           continue;
1242
1243         ArgValues tavs = avm.getOrCreateArgValues(a);
1244         switch (op)
1245         {
1246
1247         case ADDVALUE:
1248           String val = v;
1249           if (sv != null)
1250           {
1251             if (doSubs)
1252             {
1253               sv = new SubVals(sv, val, merge);
1254               val = makeSubstitutions(sv.getContent(), id);
1255             }
1256             tavs.addValue(sv, type, val, argIndex, true);
1257           }
1258           else
1259           {
1260             if (doSubs)
1261             {
1262               val = makeSubstitutions(v, id);
1263             }
1264             tavs.addValue(type, val, argIndex, true);
1265           }
1266           finaliseStoringArgValue(id, tavs);
1267           break;
1268
1269         case SETBOOLEAN:
1270           tavs.setBoolean(type, b, argIndex, true);
1271           finaliseStoringArgValue(id, tavs);
1272           break;
1273
1274         case SETNEGATED:
1275           tavs.setNegated(b, true);
1276           break;
1277
1278         case INCREMENTCOUNT:
1279           tavs.incrementCount();
1280           break;
1281
1282         default:
1283           break;
1284
1285         }
1286
1287       }
1288     }
1289     else // no wildcard linkedId -- do it simpler
1290     {
1291       switch (op)
1292       {
1293       case ADDVALUE:
1294         String val = v;
1295         if (sv != null)
1296         {
1297           if (doSubs)
1298           {
1299             val = makeSubstitutions(v, linkedId);
1300             sv = new SubVals(sv, val);
1301           }
1302           avs.addValue(sv, type, val, argIndex, false);
1303         }
1304         else
1305         {
1306           if (doSubs)
1307           {
1308             val = makeSubstitutions(v, linkedId);
1309           }
1310           avs.addValue(type, val, argIndex, false);
1311         }
1312         finaliseStoringArgValue(linkedId, avs);
1313         break;
1314
1315       case SETBOOLEAN:
1316         avs.setBoolean(type, b, argIndex, false);
1317         finaliseStoringArgValue(linkedId, avs);
1318         break;
1319
1320       case SETNEGATED:
1321         avs.setNegated(b, false);
1322         break;
1323
1324       case INCREMENTCOUNT:
1325         avs.incrementCount();
1326         break;
1327
1328       default:
1329         break;
1330       }
1331     }
1332   }
1333
1334   private ArgValuesMap getOrCreateLinkedArgValuesMap(String linkedId)
1335   {
1336     if (linkedArgs.containsKey(linkedId)
1337             && linkedArgs.get(linkedId) != null)
1338       return linkedArgs.get(linkedId);
1339
1340     linkedArgs.put(linkedId, new ArgValuesMap(linkedId));
1341     return linkedArgs.get(linkedId);
1342   }
1343
1344 }