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