JAL-629 Fix --tempfac. Hide non-working --notempfac. Add --scale, --width, --height...
[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("Invalid argument used." + System.lineSeparator()
274                   + "Use" + System.lineSeparator() + "jalview "
275                   + Arg.HELP.argString() + System.lineSeparator()
276                   + "for a usage statement.", 13);
277           continue;
278         }
279         if (a.hasOption(Opt.PRIVATE) && !allowPrivate)
280         {
281           Console.error(
282                   "Argument '" + a.argString() + "' is private. Ignoring.");
283           continue;
284         }
285         if (!a.hasOption(Opt.BOOLEAN) && negated)
286         {
287           // used "no" with a non-boolean option
288           Console.error("Argument '" + DOUBLEDASH + NEGATESTRING + argName
289                   + "' not a boolean option. Ignoring.");
290           continue;
291         }
292         if (!a.hasOption(Opt.STRING) && equalPos > -1)
293         {
294           // set --argname=value when arg does not accept values
295           Console.error("Argument '" + a.argString()
296                   + "' does not expect a value (given as '" + arg
297                   + "').  Ignoring.");
298           continue;
299         }
300         if (!a.hasOption(Opt.LINKED) && linkedId != null)
301         {
302           // set --argname[linkedId] when arg does not use linkedIds
303           Console.error("Argument '" + a.argString()
304                   + "' does not expect a linked id (given as '" + arg
305                   + "'). Ignoring.");
306           continue;
307         }
308
309         // String value(s)
310         if (a.hasOption(Opt.STRING))
311         {
312           if (equalPos >= 0)
313           {
314             if (a.hasOption(Opt.GLOB))
315             {
316               // strip off and save the SubVals to be added individually later
317               globSubVals = new SubVals(val);
318               // make substitutions before looking for files
319               String fileGlob = makeSubstitutions(globSubVals.getContent(),
320                       linkedId);
321               globVals = FileUtils.getFilenamesFromGlob(fileGlob);
322             }
323             else
324             {
325               // val is already set -- will be saved in the ArgValue later in
326               // the normal way
327             }
328           }
329           else
330           {
331             // There is no "=" so value is next arg or args (possibly shell
332             // glob-expanded)
333             if ((openEachInitialFilenames ? i : i + 1) >= args.size())
334             {
335               // no value to take for arg, which wants a value
336               Console.error("Argument '" + a.getName()
337                       + "' requires a value, none given. Ignoring.");
338               continue;
339             }
340             // deal with bash globs here (--arg val* is expanded before reaching
341             // the JVM). Note that SubVals cannot be used in this case.
342             // If using the --arg=val then the glob is preserved and Java globs
343             // will be used later. SubVals can be used.
344             if (a.hasOption(Opt.GLOB))
345             {
346               // if this is the first argument with a file list at the start of
347               // the args we add filenames from index i instead of i+1
348               globVals = getShellGlobbedFilenameValues(a, args,
349                       openEachInitialFilenames ? i : i + 1);
350             }
351             else
352             {
353               val = args.get(i + 1);
354             }
355           }
356         }
357
358         // make NOACTION adjustments
359         // default and auto counter increments
360         if (a == Arg.NPP)
361         {
362           linkedIdAutoCounter++;
363         }
364         else if (a == Arg.SUBSTITUTIONS)
365         {
366           substitutions = !negated;
367         }
368         else if (a == Arg.SETARGFILE)
369         {
370           argFile = val;
371         }
372         else if (a == Arg.UNSETARGFILE)
373         {
374           argFile = null;
375         }
376         else if (a == Arg.ALL)
377         {
378           allLinkedIds = !negated;
379         }
380
381         // this is probably only Arg.NEW and Arg.OPEN
382         if (a.hasOption(Opt.INCREMENTDEFAULTCOUNTER))
383         {
384           // use the next default prefixed OPENLINKEDID
385           defaultLinkedId(true);
386         }
387
388         String autoCounterString = null;
389         boolean usingAutoCounterLinkedId = false;
390         String defaultLinkedId = defaultLinkedId(false);
391         boolean usingDefaultLinkedId = false;
392         if (a.hasOption(Opt.LINKED))
393         {
394           if (linkedId == null)
395           {
396             if (allLinkedIds && a.hasOption(Opt.ALLOWALL))
397             {
398               linkedId = MATCHALLLINKEDIDS;
399             }
400             else
401             {
402               // use default linkedId for linked arguments
403               linkedId = defaultLinkedId;
404               usingDefaultLinkedId = true;
405               Console.debug("Changing linkedId to '" + linkedId + "' from "
406                       + arg);
407             }
408           }
409           else if (linkedId.contains(LINKEDIDAUTOCOUNTER))
410           {
411             // turn {n} to the autoCounter
412             autoCounterString = Integer.toString(linkedIdAutoCounter);
413             linkedId = linkedId.replace(LINKEDIDAUTOCOUNTER,
414                     autoCounterString);
415             usingAutoCounterLinkedId = true;
416             Console.debug(
417                     "Changing linkedId to '" + linkedId + "' from " + arg);
418           }
419           else if (linkedId.contains(INCREMENTLINKEDIDAUTOCOUNTER))
420           {
421             // turn {++n} to the incremented autoCounter
422             autoCounterString = Integer.toString(++linkedIdAutoCounter);
423             linkedId = linkedId.replace(INCREMENTLINKEDIDAUTOCOUNTER,
424                     autoCounterString);
425             usingAutoCounterLinkedId = true;
426             Console.debug(
427                     "Changing linkedId to '" + linkedId + "' from " + arg);
428           }
429         }
430
431         // do not continue in this block for NOACTION args
432         if (a.hasOption(Opt.NOACTION))
433           continue;
434
435         ArgValuesMap avm = getOrCreateLinkedArgValuesMap(linkedId);
436
437         // not dealing with both NODUPLICATEVALUES and GLOB
438         if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val))
439         {
440           Console.error("Argument '" + a.argString()
441                   + "' cannot contain a duplicate value ('" + val
442                   + "'). Ignoring this and subsequent occurrences.");
443           continue;
444         }
445
446         // check for unique id
447         SubVals idsv = new SubVals(val);
448         String id = idsv.get(ArgValues.ID);
449         if (id != null && avm.hasId(a, id))
450         {
451           Console.error("Argument '" + a.argString()
452                   + "' has a duplicate id ('" + id + "'). Ignoring.");
453           continue;
454         }
455
456         /* TODO
457          * Change all avs.addValue() avs.setBoolean avs.setNegated() avs.incrementCount calls to checkfor linkedId == "*"
458          * DONE, need to check
459          */
460         ArgValues avs = avm.getOrCreateArgValues(a);
461
462         // store appropriate String value(s)
463         if (a.hasOption(Opt.STRING))
464         {
465           if (a.hasOption(Opt.GLOB) && globVals != null
466                   && globVals.size() > 0)
467           {
468             Enumeration<String> gve = Collections.enumeration(globVals);
469             while (gve.hasMoreElements())
470             {
471               String v = gve.nextElement();
472               SubVals vsv = new SubVals(globSubVals, v);
473               addValue(linkedId, avs, vsv, v, argIndex++, true);
474               // if we're using defaultLinkedId and the arg increments the
475               // counter:
476               if (gve.hasMoreElements() && usingDefaultLinkedId
477                       && a.hasOption(Opt.INCREMENTDEFAULTCOUNTER))
478               {
479                 // increment the default linkedId
480                 linkedId = defaultLinkedId(true);
481                 // get new avm and avs
482                 avm = linkedArgs.get(linkedId);
483                 avs = avm.getOrCreateArgValues(a);
484               }
485             }
486           }
487           else
488           {
489             addValue(linkedId, avs, val, argIndex, true);
490           }
491         }
492         else if (a.hasOption(Opt.BOOLEAN))
493         {
494           setBoolean(linkedId, avs, !negated, argIndex);
495           setNegated(linkedId, avs, negated);
496         }
497         else if (a.hasOption(Opt.UNARY))
498         {
499           setBoolean(linkedId, avs, true, argIndex);
500         }
501
502         // remove the '*' linkedId that should be empty if it was created
503         if (MATCHALLLINKEDIDS.equals(linkedId)
504                 && linkedArgs.containsKey(linkedId))
505         {
506           linkedArgs.remove(linkedId);
507         }
508       }
509     }
510   }
511
512   private void finaliseStoringArgValue(String linkedId, ArgValues avs)
513   {
514     Arg a = avs.arg();
515     incrementCount(linkedId, avs);
516     argIndex++;
517
518     // store in appropriate place
519     if (a.hasOption(Opt.LINKED))
520     {
521       // store the order of linkedIds
522       if (!linkedOrder.contains(linkedId))
523         linkedOrder.add(linkedId);
524     }
525
526     // store arg in the list of args used
527     if (!argList.contains(a))
528       argList.add(a);
529   }
530
531   private String defaultLinkedId(boolean increment)
532   {
533     String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
534             .append(Integer.toString(defaultLinkedIdCounter)).toString();
535     if (increment)
536     {
537       while (linkedArgs.containsKey(defaultLinkedId))
538       {
539         defaultLinkedIdCounter++;
540         defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
541                 .append(Integer.toString(defaultLinkedIdCounter))
542                 .toString();
543       }
544     }
545     getOrCreateLinkedArgValuesMap(defaultLinkedId);
546     return defaultLinkedId;
547   }
548
549   public String makeSubstitutions(String val, String linkedId)
550   {
551     if (!this.substitutions || val == null)
552       return val;
553
554     String subvals;
555     String rest;
556     if (val.indexOf('[') == 0 && val.indexOf(']') > 1)
557     {
558       int closeBracket = val.indexOf(']');
559       if (val.length() == closeBracket)
560         return val;
561       subvals = val.substring(0, closeBracket + 1);
562       rest = val.substring(closeBracket + 1);
563     }
564     else
565     {
566       subvals = "";
567       rest = val;
568     }
569     if (rest.contains(LINKEDIDAUTOCOUNTER))
570       rest = rest.replace(LINKEDIDAUTOCOUNTER,
571               String.valueOf(linkedIdAutoCounter));
572     if (rest.contains(INCREMENTLINKEDIDAUTOCOUNTER))
573       rest = rest.replace(INCREMENTLINKEDIDAUTOCOUNTER,
574               String.valueOf(++linkedIdAutoCounter));
575     if (rest.contains(DEFAULTLINKEDIDCOUNTER))
576       rest = rest.replace(DEFAULTLINKEDIDCOUNTER,
577               String.valueOf(defaultLinkedIdCounter));
578     ArgValuesMap avm = linkedArgs.get(linkedId);
579     if (avm != null)
580     {
581       if (rest.contains(LINKEDIDBASENAME))
582       {
583         rest = rest.replace(LINKEDIDBASENAME, avm.getBasename());
584       }
585       if (rest.contains(LINKEDIDDIRNAME))
586       {
587         rest = rest.replace(LINKEDIDDIRNAME, avm.getDirname());
588       }
589     }
590     if (argFile != null)
591     {
592       if (rest.contains(ARGFILEBASENAME))
593       {
594         rest = rest.replace(ARGFILEBASENAME,
595                 FileUtils.getBasename(new File(argFile)));
596       }
597       if (rest.contains(ARGFILEDIRNAME))
598       {
599         rest = rest.replace(ARGFILEDIRNAME,
600                 FileUtils.getDirname(new File(argFile)));
601       }
602     }
603
604     return new StringBuilder(subvals).append(rest).toString();
605   }
606
607   /*
608    * A helper method to take a list of String args where we're expecting
609    * {"--previousargs", "--arg", "file1", "file2", "file3", "--otheroptionsornot"}
610    * and the index of the globbed arg, here 1. It returns a List<String> {"file1",
611    * "file2", "file3"} *and remove these from the original list object* so that
612    * processing can continue from where it has left off, e.g. args has become
613    * {"--previousargs", "--arg", "--otheroptionsornot"} so the next increment
614    * carries on from the next --arg if available.
615    */
616   protected static List<String> getShellGlobbedFilenameValues(Arg a,
617           List<String> args, int i)
618   {
619     List<String> vals = new ArrayList<>();
620     while (i < args.size() && !args.get(i).startsWith(DOUBLEDASH))
621     {
622       vals.add(FileUtils.substituteHomeDir(args.remove(i)));
623       if (!a.hasOption(Opt.GLOB))
624         break;
625     }
626     return vals;
627   }
628
629   public BootstrapArgs getBootstrapArgs()
630   {
631     return bootstrapArgs;
632   }
633
634   public boolean isSet(Arg a)
635   {
636     return a.hasOption(Opt.LINKED) ? isSetAtAll(a) : isSet(null, a);
637   }
638
639   public boolean isSetAtAll(Arg a)
640   {
641     for (String linkedId : linkedOrder)
642     {
643       if (isSet(linkedId, a))
644         return true;
645     }
646     return false;
647   }
648
649   public boolean isSet(String linkedId, Arg a)
650   {
651     ArgValuesMap avm = linkedArgs.get(linkedId);
652     return avm == null ? false : avm.containsArg(a);
653   }
654
655   public boolean getBoolean(Arg a)
656   {
657     if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY))
658     {
659       Console.warn("Getting boolean from non boolean Arg '" + a.getName()
660               + "'.");
661     }
662     return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
663   }
664
665   public boolean getBool(String linkedId, Arg a)
666   {
667     ArgValuesMap avm = linkedArgs.get(linkedId);
668     if (avm == null)
669       return a.getDefaultBoolValue();
670     ArgValues avs = avm.getArgValues(a);
671     return avs == null ? a.getDefaultBoolValue() : avs.getBoolean();
672   }
673
674   public List<String> getLinkedIds()
675   {
676     return linkedOrder;
677   }
678
679   public ArgValuesMap getLinkedArgs(String id)
680   {
681     return linkedArgs.get(id);
682   }
683
684   @Override
685   public String toString()
686   {
687     StringBuilder sb = new StringBuilder();
688     sb.append("UNLINKED\n");
689     sb.append(argValuesMapToString(linkedArgs.get(null)));
690     if (getLinkedIds() != null)
691     {
692       sb.append("LINKED\n");
693       for (String id : getLinkedIds())
694       {
695         // already listed these as UNLINKED args
696         if (id == null)
697           continue;
698
699         ArgValuesMap avm = getLinkedArgs(id);
700         sb.append("ID: '").append(id).append("'\n");
701         sb.append(argValuesMapToString(avm));
702       }
703     }
704     return sb.toString();
705   }
706
707   private static String argValuesMapToString(ArgValuesMap avm)
708   {
709     if (avm == null)
710       return null;
711     StringBuilder sb = new StringBuilder();
712     for (Arg a : avm.getArgKeys())
713     {
714       ArgValues v = avm.getArgValues(a);
715       sb.append(v.toString());
716       sb.append("\n");
717     }
718     return sb.toString();
719   }
720
721   public static ArgParser parseArgFiles(List<String> argFilenameGlobs,
722           boolean initsubstitutions, BootstrapArgs bsa)
723   {
724     List<File> argFiles = new ArrayList<>();
725
726     for (String pattern : argFilenameGlobs)
727     {
728       // I don't think we want to dedup files, making life easier
729       argFiles.addAll(FileUtils.getFilesFromGlob(pattern));
730     }
731
732     return parseArgFileList(argFiles, initsubstitutions, bsa);
733   }
734
735   public static ArgParser parseArgFileList(List<File> argFiles,
736           boolean initsubstitutions, BootstrapArgs bsa)
737   {
738     List<String> argsList = new ArrayList<>();
739     for (File argFile : argFiles)
740     {
741       if (!argFile.exists())
742       {
743         String message = Arg.ARGFILE.argString() + EQUALS + "\""
744                 + argFile.getPath() + "\": File does not exist.";
745         Jalview.exit(message, 2);
746       }
747       try
748       {
749         String setargfile = new StringBuilder(Arg.SETARGFILE.argString())
750                 .append(EQUALS).append(argFile.getCanonicalPath())
751                 .toString();
752         argsList.add(setargfile);
753         argsList.addAll(readArgFile(argFile));
754         argsList.add(Arg.UNSETARGFILE.argString());
755       } catch (IOException e)
756       {
757         String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
758                 + "\": File could not be read.";
759         Jalview.exit(message, 3);
760       }
761     }
762     // Third param "true" uses Opt.PRIVATE args --setargile=argfile and
763     // --unsetargfile
764     return new ArgParser(argsList, initsubstitutions, true, bsa);
765   }
766
767   protected static List<String> readArgFile(File argFile)
768   {
769     List<String> args = new ArrayList<>();
770     if (argFile != null && argFile.exists())
771     {
772       try
773       {
774         for (String line : Files.readAllLines(Paths.get(argFile.getPath())))
775         {
776           if (line != null && line.length() > 0
777                   && line.charAt(0) != ARGFILECOMMENT)
778             args.add(line);
779         }
780       } catch (IOException e)
781       {
782         String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
783                 + "\": File could not be read.";
784         Console.debug(message, e);
785         Jalview.exit(message, 3);
786       }
787     }
788     return args;
789   }
790
791   public static enum Position
792   {
793     FIRST, BEFORE, AFTER
794   }
795
796   // get from following Arg of type a or subval of same name (lowercase)
797   public static String getValueFromSubValOrArg(ArgValuesMap avm,
798           ArgValue av, Arg a, SubVals sv)
799   {
800     return getFromSubValArgOrPref(avm, av, a, sv, null, null, null);
801   }
802
803   // get from following Arg of type a or subval key or preference pref or
804   // default def
805   public static String getFromSubValArgOrPref(ArgValuesMap avm, ArgValue av,
806           Arg a, SubVals sv, String key, String pref, String def)
807   {
808     return getFromSubValArgOrPref(avm, a, Position.AFTER, av, sv, key, pref,
809             def);
810   }
811
812   // get from following(AFTER), first occurence of (FIRST) or previous (BEFORE)
813   // Arg of type a or subval key or preference pref or default def
814   public static String getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
815           Position pos, ArgValue av, SubVals sv, String key, String pref,
816           String def)
817   {
818     return getFromSubValArgOrPrefWithSubstitutions(null, avm, a, pos, av,
819             sv, key, pref, def);
820   }
821
822   public static String getFromSubValArgOrPrefWithSubstitutions(ArgParser ap,
823           ArgValuesMap avm, Arg a, Position pos, ArgValue av, SubVals sv,
824           String key, String pref, String def)
825   {
826     if (key == null)
827       key = a.getName();
828     String value = null;
829     if (sv != null && sv.has(key) && sv.get(key) != null)
830     {
831       value = ap == null ? sv.get(key)
832               : sv.getWithSubstitutions(ap, avm.getLinkedId(), key);
833     }
834     else if (avm != null && avm.containsArg(a))
835     {
836       if (pos == Position.FIRST && avm.getValue(a) != null)
837         value = avm.getValue(a);
838       else if (pos == Position.BEFORE
839               && avm.getClosestPreviousArgValueOfArg(av, a) != null)
840         value = avm.getClosestPreviousArgValueOfArg(av, a).getValue();
841       else if (pos == Position.AFTER
842               && avm.getClosestNextArgValueOfArg(av, a) != null)
843         value = avm.getClosestNextArgValueOfArg(av, a).getValue();
844     }
845     else
846     {
847       value = pref != null ? Cache.getDefault(pref, def) : def;
848     }
849     return value;
850   }
851
852   public static boolean getBoolFromSubValOrArg(ArgValuesMap avm, Arg a,
853           SubVals sv)
854   {
855     return getFromSubValArgOrPref(avm, a, sv, null, null, false);
856   }
857
858   public static boolean getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
859           SubVals sv, String key, String pref, boolean def)
860   {
861     if ((key == null && a == null) || (sv == null && a == null))
862       return false;
863
864     boolean usingArgKey = false;
865     if (key == null)
866     {
867       key = a.getName();
868       usingArgKey = true;
869     }
870
871     String nokey = ArgParser.NEGATESTRING + key;
872
873     // look for key or nokey in subvals first (if using Arg check options)
874     if (sv != null)
875     {
876       // check for true boolean
877       if (sv.has(key) && sv.get(key) != null)
878       {
879         if (usingArgKey)
880         {
881           if (!(a.hasOption(Opt.BOOLEAN) || a.hasOption(Opt.UNARY)))
882           {
883             Console.debug(
884                     "Looking for boolean in subval from non-boolean/non-unary Arg "
885                             + a.getName());
886             return false;
887           }
888         }
889         return sv.get(key).toLowerCase(Locale.ROOT).equals("true");
890       }
891
892       // check for negative boolean (subval "no..." will be "true")
893       if (sv.has(nokey) && sv.get(nokey) != null)
894       {
895         if (usingArgKey)
896         {
897           if (!(a.hasOption(Opt.BOOLEAN)))
898           {
899             Console.debug(
900                     "Looking for negative boolean in subval from non-boolean Arg "
901                             + a.getName());
902             return false;
903           }
904         }
905         return !sv.get(nokey).toLowerCase(Locale.ROOT).equals("true");
906       }
907     }
908
909     // check argvalues
910     if (avm != null && avm.containsArg(a))
911       return avm.getBoolean(a);
912
913     // return preference or default
914     return pref != null ? Cache.getDefault(pref, def) : def;
915   }
916
917   // the following methods look for the "*" linkedId and add the argvalue to all
918   // linkedId ArgValues if it does
919   private void addValue(String linkedId, ArgValues avs, SubVals sv,
920           String v, int argIndex, boolean doSubs)
921   {
922     Arg a = avs.arg();
923     if (MATCHALLLINKEDIDS.equals(linkedId) && a.hasOption(Opt.ALLOWALL))
924     {
925       for (String id : getLinkedIds())
926       {
927         if (id == null || MATCHALLLINKEDIDS.equals(id))
928           continue;
929         ArgValuesMap avm = linkedArgs.get(id);
930         if (a.hasOption(Opt.REQUIREINPUT)
931                 && !avm.hasArgWithOption(Opt.INPUT))
932           continue;
933         ArgValues tavs = avm.getOrCreateArgValues(a);
934         String val = v;
935         if (doSubs)
936         {
937           val = makeSubstitutions(v, id);
938           sv = new SubVals(sv, val);
939         }
940         tavs.addValue(sv, val, argIndex);
941         finaliseStoringArgValue(id, tavs);
942       }
943     }
944     else
945     {
946       String val = v;
947       if (doSubs)
948       {
949         val = makeSubstitutions(v, linkedId);
950         sv = new SubVals(sv, val);
951       }
952       avs.addValue(sv, val, argIndex);
953       finaliseStoringArgValue(linkedId, avs);
954     }
955   }
956
957   private void addValue(String linkedId, ArgValues avs, String v,
958           int argIndex, boolean doSubs)
959   {
960     Arg a = avs.arg();
961     if (MATCHALLLINKEDIDS.equals(linkedId) && a.hasOption(Opt.ALLOWALL))
962     {
963       for (String id : getLinkedIds())
964       {
965         if (id == null || MATCHALLLINKEDIDS.equals(id))
966           continue;
967         ArgValuesMap avm = linkedArgs.get(id);
968         // don't set an output if there isn't an input
969         if (a.hasOption(Opt.REQUIREINPUT)
970                 && !avm.hasArgWithOption(Opt.INPUT))
971           continue;
972         ArgValues tavs = avm.getOrCreateArgValues(a);
973         String val = doSubs ? makeSubstitutions(v, id) : v;
974         tavs.addValue(val, argIndex);
975         finaliseStoringArgValue(id, tavs);
976       }
977     }
978     else
979     {
980       String val = doSubs ? makeSubstitutions(v, linkedId) : v;
981       avs.addValue(val, argIndex);
982       finaliseStoringArgValue(linkedId, avs);
983     }
984   }
985
986   private void setBoolean(String linkedId, ArgValues avs, boolean b,
987           int argIndex)
988   {
989     Arg a = avs.arg();
990     if (MATCHALLLINKEDIDS.equals(linkedId) && a.hasOption(Opt.ALLOWALL))
991     {
992       for (String id : getLinkedIds())
993       {
994         if (id == null || MATCHALLLINKEDIDS.equals(id))
995           continue;
996         ArgValuesMap avm = linkedArgs.get(id);
997         if (a.hasOption(Opt.REQUIREINPUT)
998                 && !avm.hasArgWithOption(Opt.INPUT))
999           continue;
1000         ArgValues tavs = avm.getOrCreateArgValues(a);
1001         tavs.setBoolean(b, argIndex);
1002         finaliseStoringArgValue(id, tavs);
1003       }
1004     }
1005     else
1006     {
1007       avs.setBoolean(b, argIndex);
1008       finaliseStoringArgValue(linkedId, avs);
1009     }
1010   }
1011
1012   private void setNegated(String linkedId, ArgValues avs, boolean b)
1013   {
1014     Arg a = avs.arg();
1015     if (MATCHALLLINKEDIDS.equals(linkedId) && a.hasOption(Opt.ALLOWALL))
1016     {
1017       for (String id : getLinkedIds())
1018       {
1019         if (id == null || MATCHALLLINKEDIDS.equals(id))
1020           continue;
1021         ArgValuesMap avm = linkedArgs.get(id);
1022         if (a.hasOption(Opt.REQUIREINPUT)
1023                 && !avm.hasArgWithOption(Opt.INPUT))
1024           continue;
1025         ArgValues tavs = avm.getOrCreateArgValues(a);
1026         tavs.setNegated(b);
1027       }
1028     }
1029     else
1030     {
1031       avs.setNegated(b);
1032     }
1033   }
1034
1035   private void incrementCount(String linkedId, ArgValues avs)
1036   {
1037     Arg a = avs.arg();
1038     if (MATCHALLLINKEDIDS.equals(linkedId) && a.hasOption(Opt.ALLOWALL))
1039     {
1040       for (String id : getLinkedIds())
1041       {
1042         if (id == null || MATCHALLLINKEDIDS.equals(id))
1043           continue;
1044         ArgValuesMap avm = linkedArgs.get(id);
1045         if (a.hasOption(Opt.REQUIREINPUT)
1046                 && !avm.hasArgWithOption(Opt.INPUT))
1047           continue;
1048         ArgValues tavs = avm.getOrCreateArgValues(a);
1049         tavs.incrementCount();
1050       }
1051     }
1052     else
1053     {
1054       avs.incrementCount();
1055     }
1056   }
1057
1058   private ArgValuesMap getOrCreateLinkedArgValuesMap(String linkedId)
1059   {
1060     if (linkedArgs.containsKey(linkedId)
1061             && linkedArgs.get(linkedId) != null)
1062       return linkedArgs.get(linkedId);
1063
1064     linkedArgs.put(linkedId, new ArgValuesMap(linkedId));
1065     return linkedArgs.get(linkedId);
1066   }
1067
1068 }