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