dd863d12a1944bdad13bf9881f2c1b9bfe13cabc
[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         if (!linkedArgs.containsKey(linkedId))
434           linkedArgs.put(linkedId, new ArgValuesMap());
435
436         ArgValuesMap avm = linkedArgs.get(linkedId);
437
438         // not dealing with both NODUPLICATEVALUES and GLOB
439         if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val))
440         {
441           Console.error("Argument '" + a.argString()
442                   + "' cannot contain a duplicate value ('" + val
443                   + "'). Ignoring this and subsequent occurrences.");
444           continue;
445         }
446
447         // check for unique id
448         SubVals idsv = new SubVals(val);
449         String id = idsv.get(ArgValues.ID);
450         if (id != null && avm.hasId(a, id))
451         {
452           Console.error("Argument '" + a.argString()
453                   + "' has a duplicate id ('" + id + "'). Ignoring.");
454           continue;
455         }
456
457         /* TODO
458          * Change all avs.addValue() avs.setBoolean avs.setNegated() avs.incrementCount calls to checkfor linkedId == "*"
459          * DONE, need to check
460          */
461         ArgValues avs = avm.getOrCreateArgValues(a);
462
463         // store appropriate String value(s)
464         if (a.hasOption(Opt.STRING))
465         {
466           if (a.hasOption(Opt.GLOB) && globVals != null
467                   && globVals.size() > 0)
468           {
469             Enumeration<String> gve = Collections.enumeration(globVals);
470             while (gve.hasMoreElements())
471             {
472               String v = gve.nextElement();
473               SubVals vsv = new SubVals(globSubVals, v);
474               addValue(linkedId, avs, vsv, v, argIndex++, true);
475               // if we're using defaultLinkedId and the arg increments the
476               // counter:
477               if (gve.hasMoreElements() && usingDefaultLinkedId
478                       && a.hasOption(Opt.INCREMENTDEFAULTCOUNTER))
479               {
480                 // increment the default linkedId
481                 linkedId = defaultLinkedId(true);
482                 // get new avm and avs
483                 avm = linkedArgs.get(linkedId);
484                 avs = avm.getOrCreateArgValues(a);
485               }
486             }
487           }
488           else
489           {
490             addValue(linkedId, avs, val, argIndex, true);
491           }
492         }
493         else if (a.hasOption(Opt.BOOLEAN))
494         {
495           setBoolean(linkedId, avs, !negated, argIndex);
496           setNegated(linkedId, avs, negated);
497         }
498         else if (a.hasOption(Opt.UNARY))
499         {
500           setBoolean(linkedId, avs, true, argIndex);
501         }
502
503         // remove the '*' linkedId that should be empty if it was created
504         if (MATCHALLLINKEDIDS.equals(linkedId)
505                 && linkedArgs.containsKey(linkedId))
506         {
507           linkedArgs.remove(linkedId);
508         }
509       }
510     }
511   }
512
513   private void finaliseStoringArgValue(String linkedId, ArgValues avs)
514   {
515     Arg a = avs.arg();
516     incrementCount(linkedId, avs);
517     argIndex++;
518
519     // store in appropriate place
520     if (a.hasOption(Opt.LINKED))
521     {
522       // store the order of linkedIds
523       if (!linkedOrder.contains(linkedId))
524         linkedOrder.add(linkedId);
525     }
526
527     // store arg in the list of args used
528     if (!argList.contains(a))
529       argList.add(a);
530   }
531
532   private String defaultLinkedId(boolean increment)
533   {
534     String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
535             .append(Integer.toString(defaultLinkedIdCounter)).toString();
536     if (increment)
537     {
538       while (linkedArgs.containsKey(defaultLinkedId))
539       {
540         defaultLinkedIdCounter++;
541         defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
542                 .append(Integer.toString(defaultLinkedIdCounter))
543                 .toString();
544       }
545     }
546     if (!linkedArgs.containsKey(defaultLinkedId))
547       linkedArgs.put(defaultLinkedId, new ArgValuesMap());
548     return defaultLinkedId;
549   }
550
551   public String makeSubstitutions(String val, String linkedId)
552   {
553     if (!this.substitutions || val == null)
554       return val;
555
556     String subvals;
557     String rest;
558     if (val.indexOf('[') == 0 && val.indexOf(']') > 1)
559     {
560       int closeBracket = val.indexOf(']');
561       if (val.length() == closeBracket)
562         return val;
563       subvals = val.substring(0, closeBracket + 1);
564       rest = val.substring(closeBracket + 1);
565     }
566     else
567     {
568       subvals = "";
569       rest = val;
570     }
571     if (rest.contains(LINKEDIDAUTOCOUNTER))
572       rest = rest.replace(LINKEDIDAUTOCOUNTER,
573               String.valueOf(linkedIdAutoCounter));
574     if (rest.contains(INCREMENTLINKEDIDAUTOCOUNTER))
575       rest = rest.replace(INCREMENTLINKEDIDAUTOCOUNTER,
576               String.valueOf(++linkedIdAutoCounter));
577     if (rest.contains(DEFAULTLINKEDIDCOUNTER))
578       rest = rest.replace(DEFAULTLINKEDIDCOUNTER,
579               String.valueOf(defaultLinkedIdCounter));
580     ArgValuesMap avm = linkedArgs.get(linkedId);
581     if (avm != null)
582     {
583       if (rest.contains(LINKEDIDBASENAME))
584       {
585         rest = rest.replace(LINKEDIDBASENAME, avm.getBasename());
586       }
587       if (rest.contains(LINKEDIDDIRNAME))
588       {
589         rest = rest.replace(LINKEDIDDIRNAME, avm.getDirname());
590       }
591     }
592     if (argFile != null)
593     {
594       if (rest.contains(ARGFILEBASENAME))
595       {
596         rest = rest.replace(ARGFILEBASENAME,
597                 FileUtils.getBasename(new File(argFile)));
598       }
599       if (rest.contains(ARGFILEDIRNAME))
600       {
601         rest = rest.replace(ARGFILEDIRNAME,
602                 FileUtils.getDirname(new File(argFile)));
603       }
604     }
605
606     return new StringBuilder(subvals).append(rest).toString();
607   }
608
609   /*
610    * A helper method to take a list of String args where we're expecting
611    * {"--previousargs", "--arg", "file1", "file2", "file3", "--otheroptionsornot"}
612    * and the index of the globbed arg, here 1. It returns a List<String> {"file1",
613    * "file2", "file3"} *and remove these from the original list object* so that
614    * processing can continue from where it has left off, e.g. args has become
615    * {"--previousargs", "--arg", "--otheroptionsornot"} so the next increment
616    * carries on from the next --arg if available.
617    */
618   protected static List<String> getShellGlobbedFilenameValues(Arg a,
619           List<String> args, int i)
620   {
621     List<String> vals = new ArrayList<>();
622     while (i < args.size() && !args.get(i).startsWith(DOUBLEDASH))
623     {
624       vals.add(FileUtils.substituteHomeDir(args.remove(i)));
625       if (!a.hasOption(Opt.GLOB))
626         break;
627     }
628     return vals;
629   }
630
631   public BootstrapArgs getBootstrapArgs()
632   {
633     return bootstrapArgs;
634   }
635
636   public boolean isSet(Arg a)
637   {
638     return a.hasOption(Opt.LINKED) ? isSetAtAll(a) : isSet(null, a);
639   }
640
641   public boolean isSetAtAll(Arg a)
642   {
643     for (String linkedId : linkedOrder)
644     {
645       if (isSet(linkedId, a))
646         return true;
647     }
648     return false;
649   }
650
651   public boolean isSet(String linkedId, Arg a)
652   {
653     ArgValuesMap avm = linkedArgs.get(linkedId);
654     return avm == null ? false : avm.containsArg(a);
655   }
656
657   public boolean getBoolean(Arg a)
658   {
659     if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY))
660     {
661       Console.warn("Getting boolean from non boolean Arg '" + a.getName()
662               + "'.");
663     }
664     return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
665   }
666
667   public boolean getBool(String linkedId, Arg a)
668   {
669     ArgValuesMap avm = linkedArgs.get(linkedId);
670     if (avm == null)
671       return a.getDefaultBoolValue();
672     ArgValues avs = avm.getArgValues(a);
673     return avs == null ? a.getDefaultBoolValue() : avs.getBoolean();
674   }
675
676   public List<String> getLinkedIds()
677   {
678     return linkedOrder;
679   }
680
681   public ArgValuesMap getLinkedArgs(String id)
682   {
683     return linkedArgs.get(id);
684   }
685
686   @Override
687   public String toString()
688   {
689     StringBuilder sb = new StringBuilder();
690     sb.append("UNLINKED\n");
691     sb.append(argValuesMapToString(linkedArgs.get(null)));
692     if (getLinkedIds() != null)
693     {
694       sb.append("LINKED\n");
695       for (String id : getLinkedIds())
696       {
697         // already listed these as UNLINKED args
698         if (id == null)
699           continue;
700
701         ArgValuesMap avm = getLinkedArgs(id);
702         sb.append("ID: '").append(id).append("'\n");
703         sb.append(argValuesMapToString(avm));
704       }
705     }
706     return sb.toString();
707   }
708
709   private static String argValuesMapToString(ArgValuesMap avm)
710   {
711     if (avm == null)
712       return null;
713     StringBuilder sb = new StringBuilder();
714     for (Arg a : avm.getArgKeys())
715     {
716       ArgValues v = avm.getArgValues(a);
717       sb.append(v.toString());
718       sb.append("\n");
719     }
720     return sb.toString();
721   }
722
723   public static ArgParser parseArgFiles(List<String> argFilenameGlobs,
724           boolean initsubstitutions, BootstrapArgs bsa)
725   {
726     List<File> argFiles = new ArrayList<>();
727
728     for (String pattern : argFilenameGlobs)
729     {
730       // I don't think we want to dedup files, making life easier
731       argFiles.addAll(FileUtils.getFilesFromGlob(pattern));
732     }
733
734     return parseArgFileList(argFiles, initsubstitutions, bsa);
735   }
736
737   public static ArgParser parseArgFileList(List<File> argFiles,
738           boolean initsubstitutions, BootstrapArgs bsa)
739   {
740     List<String> argsList = new ArrayList<>();
741     for (File argFile : argFiles)
742     {
743       if (!argFile.exists())
744       {
745         String message = Arg.ARGFILE.argString() + EQUALS + "\""
746                 + argFile.getPath() + "\": File does not exist.";
747         Jalview.exit(message, 2);
748       }
749       try
750       {
751         String setargfile = new StringBuilder(Arg.SETARGFILE.argString())
752                 .append(EQUALS).append(argFile.getCanonicalPath())
753                 .toString();
754         argsList.add(setargfile);
755         argsList.addAll(readArgFile(argFile));
756         argsList.add(Arg.UNSETARGFILE.argString());
757       } catch (IOException e)
758       {
759         String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
760                 + "\": File could not be read.";
761         Jalview.exit(message, 3);
762       }
763     }
764     // Third param "true" uses Opt.PRIVATE args --setargile=argfile and
765     // --unsetargfile
766     return new ArgParser(argsList, initsubstitutions, true, bsa);
767   }
768
769   protected static List<String> readArgFile(File argFile)
770   {
771     List<String> args = new ArrayList<>();
772     if (argFile != null && argFile.exists())
773     {
774       try
775       {
776         for (String line : Files.readAllLines(Paths.get(argFile.getPath())))
777         {
778           if (line != null && line.length() > 0
779                   && line.charAt(0) != ARGFILECOMMENT)
780             args.add(line);
781         }
782       } catch (IOException e)
783       {
784         String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
785                 + "\": File could not be read.";
786         Console.debug(message, e);
787         Jalview.exit(message, 3);
788       }
789     }
790     return args;
791   }
792
793   public static enum Position
794   {
795     FIRST, BEFORE, AFTER
796   }
797
798   // get from following Arg of type a or subval of same name (lowercase)
799   public static String getValueFromSubValOrArg(ArgValuesMap avm,
800           ArgValue av, Arg a, SubVals sv)
801   {
802     return getFromSubValArgOrPref(avm, av, a, sv, null, null, null);
803   }
804
805   // get from following Arg of type a or subval key or preference pref or
806   // default def
807   public static String getFromSubValArgOrPref(ArgValuesMap avm, ArgValue av,
808           Arg a, SubVals sv, String key, String pref, String def)
809   {
810     return getFromSubValArgOrPref(avm, a, Position.AFTER, av, sv, key, pref,
811             def);
812   }
813
814   // get from following(AFTER), first occurence of (FIRST) or previous (BEFORE)
815   // Arg of type a or subval key or preference pref or default def
816   public static String getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
817           Position pos, ArgValue av, SubVals sv, String key, String pref,
818           String def)
819   {
820     if (key == null)
821       key = a.getName();
822     if (sv != null && sv.has(key) && sv.get(key) != null)
823       return sv.get(key);
824     if (avm != null && avm.containsArg(a))
825     {
826       if (pos == Position.FIRST && avm.getValue(a) != null)
827         return avm.getValue(a);
828       else if (pos == Position.BEFORE
829               && avm.getClosestPreviousArgValueOfArg(av, a) != null)
830         return avm.getClosestPreviousArgValueOfArg(av, a).getValue();
831       else if (pos == Position.AFTER
832               && avm.getClosestNextArgValueOfArg(av, a) != null)
833         return avm.getClosestNextArgValueOfArg(av, a).getValue();
834     }
835     return pref != null ? Cache.getDefault(pref, def) : def;
836   }
837
838   public static boolean getBoolFromSubValOrArg(ArgValuesMap avm, Arg a,
839           SubVals sv)
840   {
841     return getFromSubValArgOrPref(avm, a, sv, null, null, false);
842   }
843
844   public static boolean getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
845           SubVals sv, String key, String pref, boolean def)
846   {
847     if (key == null)
848       key = a.getName();
849     if (sv != null && sv.has(key) && sv.get(key) != null)
850       return sv.get(key).toLowerCase(Locale.ROOT).equals("true");
851     if (avm != null && avm.containsArg(a))
852       return avm.getBoolean(a);
853     return pref != null ? Cache.getDefault(pref, def) : def;
854   }
855
856   // the following methods look for the "*" linkedId and add the argvalue to all
857   // linkedId ArgValues if it does
858   private void addValue(String linkedId, ArgValues avs, SubVals sv,
859           String v, int argIndex, boolean doSubs)
860   {
861     Arg a = avs.arg();
862     if (MATCHALLLINKEDIDS.equals(linkedId) && a.hasOption(Opt.ALLOWALL))
863     {
864       for (String id : getLinkedIds())
865       {
866         if (id == null || MATCHALLLINKEDIDS.equals(id))
867           continue;
868         ArgValuesMap avm = linkedArgs.get(id);
869         if (a.hasOption(Opt.REQUIREINPUT)
870                 && !avm.hasArgWithOption(Opt.INPUT))
871           continue;
872         ArgValues tavs = avm.getOrCreateArgValues(a);
873         String val = v;
874         if (doSubs)
875         {
876           val = makeSubstitutions(v, id);
877           sv = new SubVals(sv, val);
878         }
879         tavs.addValue(sv, val, argIndex);
880         finaliseStoringArgValue(id, tavs);
881       }
882     }
883     else
884     {
885       String val = v;
886       if (doSubs)
887       {
888         val = makeSubstitutions(v, linkedId);
889         sv = new SubVals(sv, val);
890       }
891       avs.addValue(sv, val, argIndex);
892       finaliseStoringArgValue(linkedId, avs);
893     }
894   }
895
896   private void addValue(String linkedId, ArgValues avs, String v,
897           int argIndex, boolean doSubs)
898   {
899     Arg a = avs.arg();
900     if (MATCHALLLINKEDIDS.equals(linkedId) && a.hasOption(Opt.ALLOWALL))
901     {
902       for (String id : getLinkedIds())
903       {
904         if (id == null || MATCHALLLINKEDIDS.equals(id))
905           continue;
906         ArgValuesMap avm = linkedArgs.get(id);
907         // don't set an output if there isn't an input
908         if (a.hasOption(Opt.REQUIREINPUT)
909                 && !avm.hasArgWithOption(Opt.INPUT))
910           continue;
911         ArgValues tavs = avm.getOrCreateArgValues(a);
912         String val = doSubs ? makeSubstitutions(v, id) : v;
913         tavs.addValue(val, argIndex);
914         finaliseStoringArgValue(id, tavs);
915       }
916     }
917     else
918     {
919       String val = doSubs ? makeSubstitutions(v, linkedId) : v;
920       avs.addValue(val, argIndex);
921       finaliseStoringArgValue(linkedId, avs);
922     }
923   }
924
925   private void setBoolean(String linkedId, ArgValues avs, boolean b,
926           int argIndex)
927   {
928     Arg a = avs.arg();
929     if (MATCHALLLINKEDIDS.equals(linkedId) && a.hasOption(Opt.ALLOWALL))
930     {
931       for (String id : getLinkedIds())
932       {
933         if (id == null || MATCHALLLINKEDIDS.equals(id))
934           continue;
935         ArgValuesMap avm = linkedArgs.get(id);
936         if (a.hasOption(Opt.REQUIREINPUT)
937                 && !avm.hasArgWithOption(Opt.INPUT))
938           continue;
939         ArgValues tavs = avm.getOrCreateArgValues(a);
940         tavs.setBoolean(b, argIndex);
941         finaliseStoringArgValue(id, tavs);
942       }
943     }
944     else
945     {
946       avs.setBoolean(b, argIndex);
947       finaliseStoringArgValue(linkedId, avs);
948     }
949   }
950
951   private void setNegated(String linkedId, ArgValues avs, boolean b)
952   {
953     Arg a = avs.arg();
954     if (MATCHALLLINKEDIDS.equals(linkedId) && a.hasOption(Opt.ALLOWALL))
955     {
956       for (String id : getLinkedIds())
957       {
958         if (id == null || MATCHALLLINKEDIDS.equals(id))
959           continue;
960         ArgValuesMap avm = linkedArgs.get(id);
961         if (a.hasOption(Opt.REQUIREINPUT)
962                 && !avm.hasArgWithOption(Opt.INPUT))
963           continue;
964         ArgValues tavs = avm.getOrCreateArgValues(a);
965         tavs.setNegated(b);
966       }
967     }
968     else
969     {
970       avs.setNegated(b);
971     }
972   }
973
974   private void incrementCount(String linkedId, ArgValues avs)
975   {
976     Arg a = avs.arg();
977     if (MATCHALLLINKEDIDS.equals(linkedId) && a.hasOption(Opt.ALLOWALL))
978     {
979       for (String id : getLinkedIds())
980       {
981         if (id == null || MATCHALLLINKEDIDS.equals(id))
982           continue;
983         ArgValuesMap avm = linkedArgs.get(id);
984         if (a.hasOption(Opt.REQUIREINPUT)
985                 && !avm.hasArgWithOption(Opt.INPUT))
986           continue;
987         ArgValues tavs = avm.getOrCreateArgValues(a);
988         tavs.incrementCount();
989       }
990     }
991     else
992     {
993       avs.incrementCount();
994     }
995   }
996
997 }