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