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