JAL-629 Change names of Opt.ALLOWALL to Opt.ALLOWMULTIID and Opt.MULTI to Opt.MULTIVA...
[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.bin.argparser.Arg.Type;
42 import jalview.util.FileUtils;
43 import jalview.util.HttpUtils;
44
45 public class ArgParser
46 {
47   protected static final String SINGLEDASH = "-";
48
49   protected static final String DOUBLEDASH = "--";
50
51   public static final char EQUALS = '=';
52
53   public static final String STDOUTFILENAME = "-";
54
55   protected static final String NEGATESTRING = "no";
56
57   /**
58    * the default linked id prefix used for no id (ie when not even square braces
59    * are provided)
60    */
61   protected static final String DEFAULTLINKEDIDPREFIX = "JALVIEW:";
62
63   /**
64    * the linkedId string used to match all linkedIds seen so far
65    */
66   protected static final String MATCHALLLINKEDIDS = "*";
67
68   /**
69    * the linkedId string used to match all of the last --open'ed linkedIds
70    */
71   protected static final String MATCHOPENEDLINKEDIDS = "open*";
72
73   /**
74    * the counter added to the default linked id prefix
75    */
76   private int defaultLinkedIdCounter = 0;
77
78   /**
79    * the substitution string used to use the defaultLinkedIdCounter
80    */
81   private static final String DEFAULTLINKEDIDCOUNTER = "{}";
82
83   /**
84    * the linked id prefix used for --open files. NOW the same as DEFAULT
85    */
86   protected static final String OPENLINKEDIDPREFIX = DEFAULTLINKEDIDPREFIX;
87
88   /**
89    * the counter used for {n} substitutions
90    */
91   private int linkedIdAutoCounter = 0;
92
93   /**
94    * the linked id substitution string used to increment the idCounter (and use
95    * the incremented value)
96    */
97   private static final String INCREMENTLINKEDIDAUTOCOUNTER = "{++n}";
98
99   /**
100    * the linked id substitution string used to use the idCounter
101    */
102   private static final String LINKEDIDAUTOCOUNTER = "{n}";
103
104   /**
105    * the linked id substitution string used to use the filename extension of
106    * --append or --open
107    */
108   private static final String LINKEDIDEXTENSION = "{extension}";
109
110   /**
111    * the linked id substitution string used to use the base filename of --append
112    */
113   /** or --open */
114   private static final String LINKEDIDBASENAME = "{basename}";
115
116   /**
117    * the linked id substitution string used to use the dir path of --append or
118    * --open
119    */
120   private static final String LINKEDIDDIRNAME = "{dirname}";
121
122   /**
123    * the current argfile
124    */
125   private String argFile = null;
126
127   /**
128    * the linked id substitution string used to use the dir path of the latest
129    */
130   /** --argfile name */
131   private static final String ARGFILEBASENAME = "{argfilebasename}";
132
133   /**
134    * the linked id substitution string used to use the dir path of the latest
135    * --argfile name
136    */
137   private static final String ARGFILEDIRNAME = "{argfiledirname}";
138
139   /**
140    * flag to say whether {n} subtitutions in output filenames should be made.
141    * Turn on and off with --substitutions and --nosubstitutions Start with it on
142    */
143   private boolean substitutions = true;
144
145   /**
146    * flag to say whether the default linkedId is the current default linked id
147    *
148    * or ALL linkedIds
149    */
150   private boolean allLinkedIds = false;
151
152   /**
153    * flag to say whether the structure arguments should be applied to all
154    * structures with this linked id
155    */
156   private boolean allStructures = false;
157
158   protected static final Map<String, Arg> argMap;
159
160   protected Map<String, ArgValuesMap> linkedArgs = new HashMap<>();
161
162   protected List<String> linkedOrder = new ArrayList<>();
163
164   protected List<String> storedLinkedIds = new ArrayList<>();
165
166   protected List<Arg> argList = new ArrayList<>();
167
168   private static final char ARGFILECOMMENT = '#';
169
170   private int argIndex = 0;
171
172   private BootstrapArgs bootstrapArgs = null;
173
174   static
175   {
176     argMap = new HashMap<>();
177     for (Arg a : EnumSet.allOf(Arg.class))
178     {
179       for (String argName : a.getNames())
180       {
181         if (argMap.containsKey(argName))
182         {
183           Console.warn("Trying to add argument name multiple times: '"
184                   + argName + "'");
185           if (argMap.get(argName) != a)
186           {
187             Console.error(
188                     "Trying to add argument name multiple times for different Args: '"
189                             + argMap.get(argName).getName() + ":" + argName
190                             + "' and '" + a.getName() + ":" + argName
191                             + "'");
192           }
193           continue;
194         }
195         argMap.put(argName, a);
196       }
197     }
198   }
199
200   public ArgParser(String[] args)
201   {
202     this(args, false, null);
203   }
204
205   public ArgParser(String[] args, boolean initsubstitutions,
206           BootstrapArgs bsa)
207   {
208     /*
209      *  Make a mutable new ArrayList so that shell globbing parser works.
210      * (When shell file globbing is used, there are a sequence of non-Arg
211      * arguments (which are the expanded globbed filenames) that need to be
212      * consumed by the --append/--argfile/etc Arg which is most easily done
213      * by removing these filenames from the list one at a time. This can't be
214      * done with an ArrayList made with only Arrays.asList(String[] args) as
215      * that is not mutable. )
216      */
217     this(new ArrayList<>(Arrays.asList(args)), initsubstitutions, false,
218             bsa);
219   }
220
221   public ArgParser(List<String> args, boolean initsubstitutions)
222   {
223     this(args, initsubstitutions, false, null);
224   }
225
226   public ArgParser(List<String> args, boolean initsubstitutions,
227           boolean allowPrivate, BootstrapArgs bsa)
228   {
229     // do nothing if there are no "--" args and (some "-" args || >0 arg is
230     // "open")
231     boolean d = false;
232     boolean dd = false;
233     for (String arg : args)
234     {
235       if (arg.startsWith(DOUBLEDASH))
236       {
237         dd = true;
238         break;
239       }
240       else if (arg.startsWith("-") || arg.equals("open"))
241       {
242         d = true;
243       }
244     }
245     if (d && !dd)
246     {
247       // leave it to the old style -- parse an empty list
248       parse(new ArrayList<String>(), false, false);
249       return;
250     }
251     if (bsa != null)
252       this.bootstrapArgs = bsa;
253     else
254       this.bootstrapArgs = BootstrapArgs.getBootstrapArgs(args);
255     parse(args, initsubstitutions, allowPrivate);
256   }
257
258   private void parse(List<String> args, boolean initsubstitutions,
259           boolean allowPrivate)
260   {
261     this.substitutions = initsubstitutions;
262
263     /*
264      *  If the first argument does not start with "--" or "-" or is not "open",
265      *  and is a filename that exists or a URL, it is probably a file/list of
266      *  files to open so we insert an Arg.OPEN argument before it. This will
267      *  mean the list of files at the start of the arguments are all opened
268      *  separately.
269      */
270     if (args.size() > 0)
271     {
272       String arg0 = args.get(0);
273       if (arg0 != null
274               && (!arg0.startsWith(DOUBLEDASH) && !arg0.startsWith("-")
275                       && !arg0.equals("open") && (new File(arg0).exists()
276                               || HttpUtils.startsWithHttpOrHttps(arg0))))
277       {
278         // insert "--open" at the start
279         args.add(0, Arg.OPEN.argString());
280       }
281     }
282
283     for (int i = 0; i < args.size(); i++)
284     {
285       String arg = args.get(i);
286
287       // look for double-dash, e.g. --arg
288       if (arg.startsWith(DOUBLEDASH))
289       {
290         String argName = null;
291         String val = null;
292         List<String> globVals = null; // for Opt.GLOB only
293         SubVals globSubVals = null; // also for use by Opt.GLOB only
294         String linkedId = null;
295         Type type = null;
296
297         // look for equals e.g. --arg=value
298         int equalPos = arg.indexOf(EQUALS);
299         if (equalPos > -1)
300         {
301           argName = arg.substring(DOUBLEDASH.length(), equalPos);
302           val = arg.substring(equalPos + 1);
303         }
304         else
305         {
306           argName = arg.substring(DOUBLEDASH.length());
307         }
308
309         // look for linked ID e.g. --arg[linkedID]
310         int idOpen = argName.indexOf('[');
311         int idClose = argName.indexOf(']');
312         if (idOpen > -1 && idClose == argName.length() - 1)
313         {
314           linkedId = argName.substring(idOpen + 1, idClose);
315           argName = argName.substring(0, idOpen);
316         }
317
318         // look for type modification e.g. --help-opening
319         int dashPos = argName.indexOf(SINGLEDASH);
320         if (dashPos > -1)
321         {
322           String potentialArgName = argName.substring(0, dashPos);
323           Arg potentialArg = argMap.get(potentialArgName);
324           if (potentialArg != null && potentialArg.hasOption(Opt.HASTYPE))
325           {
326             String typeName = argName.substring(dashPos + 1);
327             try
328             {
329               type = Type.valueOf(typeName);
330             } catch (IllegalArgumentException e)
331             {
332               type = Type.INVALID;
333             }
334             argName = argName.substring(0, dashPos);
335           }
336         }
337
338         Arg a = argMap.get(argName);
339         // check for boolean prepended by "no" e.g. --nowrap
340         boolean negated = false;
341         if (a == null)
342         {
343           if (argName.startsWith(NEGATESTRING) && argMap
344                   .containsKey(argName.substring(NEGATESTRING.length())))
345           {
346             argName = argName.substring(NEGATESTRING.length());
347             a = argMap.get(argName);
348             negated = true;
349           }
350           else
351           {
352             // after all other args, look for Opt.PREFIXKEV args if still not
353             // found
354             for (Arg potentialArg : EnumSet.allOf(Arg.class))
355             {
356               if (potentialArg.hasOption(Opt.PREFIXKEV) && argName != null
357                       && argName.startsWith(potentialArg.getName())
358                       && equalPos > -1)
359               {
360                 val = argName.substring(potentialArg.getName().length())
361                         + EQUALS + val;
362                 argName = argName.substring(0,
363                         potentialArg.getName().length());
364                 a = potentialArg;
365                 break;
366               }
367             }
368           }
369         }
370
371         // check for config errors
372         if (a == null)
373         {
374           // arg not found
375           Console.error("Argument '" + arg + "' not recognised.  Exiting.");
376           Jalview.exit("Invalid argument used." + System.lineSeparator()
377                   + "Use" + System.lineSeparator() + "jalview "
378                   + Arg.HELP.argString() + System.lineSeparator()
379                   + "for a usage statement.", 13);
380           continue;
381         }
382         if (a.hasOption(Opt.PRIVATE) && !allowPrivate)
383         {
384           Console.error(
385                   "Argument '" + a.argString() + "' is private. Ignoring.");
386           continue;
387         }
388         if (!a.hasOption(Opt.BOOLEAN) && negated)
389         {
390           // used "no" with a non-boolean option
391           Console.error("Argument '" + DOUBLEDASH + NEGATESTRING + argName
392                   + "' not a boolean option. Ignoring.");
393           continue;
394         }
395         if (!a.hasOption(Opt.STRING) && equalPos > -1)
396         {
397           // set --argname=value when arg does not accept values
398           Console.error("Argument '" + a.argString()
399                   + "' does not expect a value (given as '" + arg
400                   + "').  Ignoring.");
401           continue;
402         }
403         if (!a.hasOption(Opt.LINKED) && linkedId != null)
404         {
405           // set --argname[linkedId] when arg does not use linkedIds
406           Console.error("Argument '" + a.argString()
407                   + "' does not expect a linked id (given as '" + arg
408                   + "'). Ignoring.");
409           continue;
410         }
411
412         // String value(s)
413         if (a.hasOption(Opt.STRING))
414         {
415           if (equalPos >= 0)
416           {
417             if (a.hasOption(Opt.GLOB))
418             {
419               // strip off and save the SubVals to be added individually later
420               globSubVals = new SubVals(val);
421               // make substitutions before looking for files
422               String fileGlob = makeSubstitutions(globSubVals.getContent(),
423                       linkedId);
424               globVals = FileUtils.getFilenamesFromGlob(fileGlob);
425             }
426             else
427             {
428               // val is already set -- will be saved in the ArgValue later in
429               // the normal way
430             }
431           }
432           else
433           {
434             // There is no "=" so value is next arg or args (possibly shell
435             // glob-expanded)
436             if (i + 1 >= args.size())
437             {
438               // no value to take for arg, which wants a value
439               Console.error("Argument '" + a.getName()
440                       + "' requires a value, none given. Ignoring.");
441               continue;
442             }
443             // deal with bash globs here (--arg val* is expanded before reaching
444             // the JVM). Note that SubVals cannot be used in this case.
445             // If using the --arg=val then the glob is preserved and Java globs
446             // will be used later. SubVals can be used.
447             if (a.hasOption(Opt.GLOB))
448             {
449               // if this is the first argument with a file list at the start of
450               // the args we add filenames from index i instead of i+1
451               globVals = getShellGlobbedFilenameValues(a, args, i + 1);
452             }
453             else
454             {
455               val = args.get(i + 1);
456             }
457           }
458         }
459
460         // make NOACTION adjustments
461         // default and auto counter increments
462         if (a == Arg.NPP)
463         {
464           linkedIdAutoCounter++;
465         }
466         else if (a == Arg.SUBSTITUTIONS)
467         {
468           substitutions = !negated;
469         }
470         else if (a == Arg.SETARGFILE)
471         {
472           argFile = val;
473         }
474         else if (a == Arg.UNSETARGFILE)
475         {
476           argFile = null;
477         }
478         else if (a == Arg.ALL)
479         {
480           allLinkedIds = !negated;
481         }
482         else if (a == Arg.ALLSTRUCTURES)
483         {
484           allStructures = !negated;
485         }
486
487         if (a.hasOption(Opt.STORED))
488         {
489           // reset the lastOpenedLinkedIds list
490           this.storedLinkedIds = new ArrayList<>();
491         }
492
493         // this is probably only Arg.NEW and Arg.OPEN
494         if (a.hasOption(Opt.INCREMENTDEFAULTCOUNTER))
495         {
496           // use the next default prefixed OPENLINKEDID
497           defaultLinkedId(true);
498         }
499
500         String autoCounterString = null;
501         String defaultLinkedId = defaultLinkedId(false);
502         boolean usingDefaultLinkedId = false;
503         if (a.hasOption(Opt.LINKED))
504         {
505           if (linkedId == null)
506           {
507             if (a.hasOption(Opt.OUTPUTFILE) && a.hasOption(Opt.ALLOWMULTIID)
508                     && val.startsWith(MATCHALLLINKEDIDS))
509             {
510               // --output=*.ext is shorthand for --all --output {basename}.ext
511               // (or --image=*.ext)
512               allLinkedIds = true;
513               linkedId = MATCHALLLINKEDIDS;
514               val = LINKEDIDDIRNAME + File.separator + LINKEDIDBASENAME
515                       + val.substring(MATCHALLLINKEDIDS.length());
516             }
517             else if (allLinkedIds && a.hasOption(Opt.ALLOWMULTIID))
518             {
519               linkedId = MATCHALLLINKEDIDS;
520             }
521             else if (a.hasOption(Opt.ALLOWMULTIID)
522                     && this.storedLinkedIds != null
523                     && this.storedLinkedIds.size() > 0)
524             {
525               linkedId = MATCHOPENEDLINKEDIDS;
526             }
527             else
528             {
529               // use default linkedId for linked arguments
530               linkedId = defaultLinkedId;
531               usingDefaultLinkedId = true;
532               Console.debug("Changing linkedId to '" + linkedId + "' from "
533                       + arg);
534             }
535           }
536           else
537           {
538             if (linkedId.contains(LINKEDIDAUTOCOUNTER))
539             {
540               // turn {n} to the autoCounter
541               autoCounterString = Integer.toString(linkedIdAutoCounter);
542               linkedId = linkedId.replace(LINKEDIDAUTOCOUNTER,
543                       autoCounterString);
544               Console.debug("Changing linkedId to '" + linkedId + "' from "
545                       + arg);
546             }
547             if (linkedId.contains(INCREMENTLINKEDIDAUTOCOUNTER))
548             {
549               // turn {++n} to the incremented autoCounter
550               autoCounterString = Integer.toString(++linkedIdAutoCounter);
551               linkedId = linkedId.replace(INCREMENTLINKEDIDAUTOCOUNTER,
552                       autoCounterString);
553               Console.debug("Changing linkedId to '" + linkedId + "' from "
554                       + arg);
555             }
556           }
557         }
558
559         // do not continue in this block for NOACTION args
560         if (a.hasOption(Opt.NOACTION))
561           continue;
562
563         ArgValuesMap avm = getOrCreateLinkedArgValuesMap(linkedId);
564
565         // not dealing with both NODUPLICATEVALUES and GLOB
566         if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val))
567         {
568           Console.error("Argument '" + a.argString()
569                   + "' cannot contain a duplicate value ('" + val
570                   + "'). Ignoring this and subsequent occurrences.");
571           continue;
572         }
573
574         // check for unique id
575         SubVals subvals = new SubVals(val);
576         boolean addNewSubVals = false;
577         String id = subvals.get(ArgValues.ID);
578         if (id != null && avm.hasId(a, id))
579         {
580           Console.error("Argument '" + a.argString()
581                   + "' has a duplicate id ('" + id + "'). Ignoring.");
582           continue;
583         }
584
585         // set allstructures to all non-primary structure options in this linked
586         // id if --allstructures has been set
587         if (allStructures
588                 && (a.getType() == Type.STRUCTURE
589                         || a.getType() == Type.STRUCTUREIMAGE)
590                 && !a.hasOption(Opt.PRIMARY))
591         {
592           if (!subvals.has(Arg.ALLSTRUCTURES.getName()))
593           // && !subvals.has("structureid"))
594           {
595             subvals.put(Arg.ALLSTRUCTURES.getName(), "true");
596             addNewSubVals = true;
597           }
598         }
599
600         ArgValues avs = avm.getOrCreateArgValues(a);
601
602         // store appropriate String value(s)
603         if (a.hasOption(Opt.STRING))
604         {
605           if (a.hasOption(Opt.GLOB) && globVals != null
606                   && globVals.size() > 0)
607           {
608             Enumeration<String> gve = Collections.enumeration(globVals);
609             while (gve.hasMoreElements())
610             {
611               String v = gve.nextElement();
612               SubVals vsv = new SubVals(globSubVals, v);
613               addValue(linkedId, type, avs, vsv, v, argIndex++, true);
614               // if we're using defaultLinkedId and the arg increments the
615               // counter:
616               if (gve.hasMoreElements() && usingDefaultLinkedId
617                       && a.hasOption(Opt.INCREMENTDEFAULTCOUNTER))
618               {
619                 // increment the default linkedId
620                 linkedId = defaultLinkedId(true);
621                 // get new avm and avs
622                 avm = linkedArgs.get(linkedId);
623                 avs = avm.getOrCreateArgValues(a);
624               }
625             }
626           }
627           else
628           {
629             // addValue(linkedId, type, avs, val, argIndex, true);
630             addValue(linkedId, type, avs, addNewSubVals ? subvals : null,
631                     val, argIndex, true);
632           }
633         }
634         else if (a.hasOption(Opt.BOOLEAN))
635         {
636           setBoolean(linkedId, type, avs, !negated, argIndex);
637           setNegated(linkedId, avs, negated);
638         }
639         else if (a.hasOption(Opt.UNARY))
640         {
641           setBoolean(linkedId, type, avs, true, argIndex);
642         }
643
644         // remove the '*' or 'open*' linkedId that should be empty if it was
645         // created
646         if ((MATCHALLLINKEDIDS.equals(linkedId)
647                 || MATCHOPENEDLINKEDIDS.equals(linkedId))
648                 && linkedArgs.containsKey(linkedId))
649         {
650           linkedArgs.remove(linkedId);
651         }
652       }
653     }
654   }
655
656   private void finaliseStoringArgValue(String linkedId, ArgValues avs)
657   {
658     Arg a = avs.arg();
659     incrementCount(linkedId, avs);
660     argIndex++;
661
662     // store in appropriate place
663     if (a.hasOption(Opt.LINKED))
664     {
665       // store the order of linkedIds
666       if (!linkedOrder.contains(linkedId))
667         linkedOrder.add(linkedId);
668     }
669
670     // store arg in the list of args used
671     if (!argList.contains(a))
672       argList.add(a);
673   }
674
675   private String defaultLinkedId(boolean increment)
676   {
677     String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
678             .append(Integer.toString(defaultLinkedIdCounter)).toString();
679     if (increment)
680     {
681       while (linkedArgs.containsKey(defaultLinkedId))
682       {
683         defaultLinkedIdCounter++;
684         defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
685                 .append(Integer.toString(defaultLinkedIdCounter))
686                 .toString();
687       }
688     }
689     getOrCreateLinkedArgValuesMap(defaultLinkedId);
690     return defaultLinkedId;
691   }
692
693   public String makeSubstitutions(String val, String linkedId)
694   {
695     if (!this.substitutions || val == null)
696       return val;
697
698     String subvals;
699     String rest;
700     if (val.indexOf('[') == 0 && val.indexOf(']') > 1)
701     {
702       int closeBracket = val.indexOf(']');
703       if (val.length() == closeBracket)
704         return val;
705       subvals = val.substring(0, closeBracket + 1);
706       rest = val.substring(closeBracket + 1);
707     }
708     else
709     {
710       subvals = "";
711       rest = val;
712     }
713     if (rest.contains(LINKEDIDAUTOCOUNTER))
714       rest = rest.replace(LINKEDIDAUTOCOUNTER,
715               String.valueOf(linkedIdAutoCounter));
716     if (rest.contains(INCREMENTLINKEDIDAUTOCOUNTER))
717       rest = rest.replace(INCREMENTLINKEDIDAUTOCOUNTER,
718               String.valueOf(++linkedIdAutoCounter));
719     if (rest.contains(DEFAULTLINKEDIDCOUNTER))
720       rest = rest.replace(DEFAULTLINKEDIDCOUNTER,
721               String.valueOf(defaultLinkedIdCounter));
722     ArgValuesMap avm = linkedArgs.get(linkedId);
723     if (avm != null)
724     {
725       if (rest.contains(LINKEDIDBASENAME))
726       {
727         rest = rest.replace(LINKEDIDBASENAME, avm.getBasename());
728       }
729       if (rest.contains(LINKEDIDEXTENSION))
730       {
731         rest = rest.replace(LINKEDIDEXTENSION, avm.getExtension());
732       }
733       if (rest.contains(LINKEDIDDIRNAME))
734       {
735         rest = rest.replace(LINKEDIDDIRNAME, avm.getDirname());
736       }
737     }
738     if (argFile != null)
739     {
740       if (rest.contains(ARGFILEBASENAME))
741       {
742         rest = rest.replace(ARGFILEBASENAME,
743                 FileUtils.getBasename(new File(argFile)));
744       }
745       if (rest.contains(ARGFILEDIRNAME))
746       {
747         rest = rest.replace(ARGFILEDIRNAME,
748                 FileUtils.getDirname(new File(argFile)));
749       }
750     }
751
752     return new StringBuilder(subvals).append(rest).toString();
753   }
754
755   /*
756    * A helper method to take a list of String args where we're expecting
757    * {"--previousargs", "--arg", "file1", "file2", "file3", "--otheroptionsornot"}
758    * and the index of the globbed arg, here 1. It returns a List<String> {"file1",
759    * "file2", "file3"} *and remove these from the original list object* so that
760    * processing can continue from where it has left off, e.g. args has become
761    * {"--previousargs", "--arg", "--otheroptionsornot"} so the next increment
762    * carries on from the next --arg if available.
763    */
764   protected static List<String> getShellGlobbedFilenameValues(Arg a,
765           List<String> args, int i)
766   {
767     List<String> vals = new ArrayList<>();
768     while (i < args.size() && !args.get(i).startsWith(DOUBLEDASH))
769     {
770       vals.add(FileUtils.substituteHomeDir(args.remove(i)));
771       if (!a.hasOption(Opt.GLOB))
772         break;
773     }
774     return vals;
775   }
776
777   public BootstrapArgs getBootstrapArgs()
778   {
779     return bootstrapArgs;
780   }
781
782   public boolean isSet(Arg a)
783   {
784     return a.hasOption(Opt.LINKED) ? isSetAtAll(a) : isSet(null, a);
785   }
786
787   public boolean isSetAtAll(Arg a)
788   {
789     for (String linkedId : linkedOrder)
790     {
791       if (isSet(linkedId, a))
792         return true;
793     }
794     return false;
795   }
796
797   public boolean isSet(String linkedId, Arg a)
798   {
799     ArgValuesMap avm = linkedArgs.get(linkedId);
800     return avm == null ? false : avm.containsArg(a);
801   }
802
803   public boolean getBoolean(Arg a)
804   {
805     if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY))
806     {
807       Console.warn("Getting boolean from non boolean Arg '" + a.getName()
808               + "'.");
809     }
810     return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
811   }
812
813   public boolean getBool(String linkedId, Arg a)
814   {
815     ArgValuesMap avm = linkedArgs.get(linkedId);
816     if (avm == null)
817       return a.getDefaultBoolValue();
818     ArgValues avs = avm.getArgValues(a);
819     return avs == null ? a.getDefaultBoolValue() : avs.getBoolean();
820   }
821
822   public List<String> getLinkedIds()
823   {
824     return linkedOrder;
825   }
826
827   public ArgValuesMap getLinkedArgs(String id)
828   {
829     return linkedArgs.get(id);
830   }
831
832   @Override
833   public String toString()
834   {
835     StringBuilder sb = new StringBuilder();
836     sb.append("UNLINKED\n");
837     sb.append(argValuesMapToString(linkedArgs.get(null)));
838     if (getLinkedIds() != null)
839     {
840       sb.append("LINKED\n");
841       for (String id : getLinkedIds())
842       {
843         // already listed these as UNLINKED args
844         if (id == null)
845           continue;
846
847         ArgValuesMap avm = getLinkedArgs(id);
848         sb.append("ID: '").append(id).append("'\n");
849         sb.append(argValuesMapToString(avm));
850       }
851     }
852     return sb.toString();
853   }
854
855   private static String argValuesMapToString(ArgValuesMap avm)
856   {
857     if (avm == null)
858       return null;
859     StringBuilder sb = new StringBuilder();
860     for (Arg a : avm.getArgKeys())
861     {
862       ArgValues v = avm.getArgValues(a);
863       sb.append(v.toString());
864       sb.append("\n");
865     }
866     return sb.toString();
867   }
868
869   public static ArgParser parseArgFiles(List<String> argFilenameGlobs,
870           boolean initsubstitutions, BootstrapArgs bsa)
871   {
872     List<File> argFiles = new ArrayList<>();
873
874     for (String pattern : argFilenameGlobs)
875     {
876       // I don't think we want to dedup files, making life easier
877       argFiles.addAll(FileUtils.getFilesFromGlob(pattern));
878     }
879
880     return parseArgFileList(argFiles, initsubstitutions, bsa);
881   }
882
883   public static ArgParser parseArgFileList(List<File> argFiles,
884           boolean initsubstitutions, BootstrapArgs bsa)
885   {
886     List<String> argsList = new ArrayList<>();
887     for (File argFile : argFiles)
888     {
889       if (!argFile.exists())
890       {
891         String message = Arg.ARGFILE.argString() + EQUALS + "\""
892                 + argFile.getPath() + "\": File does not exist.";
893         Jalview.exit(message, 2);
894       }
895       try
896       {
897         String setargfile = new StringBuilder(Arg.SETARGFILE.argString())
898                 .append(EQUALS).append(argFile.getCanonicalPath())
899                 .toString();
900         argsList.add(setargfile);
901         argsList.addAll(readArgFile(argFile));
902         argsList.add(Arg.UNSETARGFILE.argString());
903       } catch (IOException e)
904       {
905         String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
906                 + "\": File could not be read.";
907         Jalview.exit(message, 3);
908       }
909     }
910     // Third param "true" uses Opt.PRIVATE args --setargile=argfile and
911     // --unsetargfile
912     return new ArgParser(argsList, initsubstitutions, true, bsa);
913   }
914
915   protected static List<String> readArgFile(File argFile)
916   {
917     List<String> args = new ArrayList<>();
918     if (argFile != null && argFile.exists())
919     {
920       try
921       {
922         for (String line : Files.readAllLines(Paths.get(argFile.getPath())))
923         {
924           if (line != null && line.length() > 0
925                   && line.charAt(0) != ARGFILECOMMENT)
926             args.add(line);
927         }
928       } catch (IOException e)
929       {
930         String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
931                 + "\": File could not be read.";
932         Console.debug(message, e);
933         Jalview.exit(message, 3);
934       }
935     }
936     return args;
937   }
938
939   public static enum Position
940   {
941     FIRST, BEFORE, AFTER
942   }
943
944   /**
945    * get from following Arg of type a or subval of same name (lowercase)
946    */
947   public static String getValueFromSubValOrArg(ArgValuesMap avm,
948           ArgValue av, Arg a, SubVals sv)
949   {
950     return getFromSubValArgOrPref(avm, av, a, sv, null, null, null);
951   }
952
953   /**
954    * get from following Arg of type a or subval key or preference pref or
955    * default def
956    */
957   public static String getFromSubValArgOrPref(ArgValuesMap avm, ArgValue av,
958           Arg a, SubVals sv, String key, String pref, String def)
959   {
960     return getFromSubValArgOrPref(avm, a, Position.AFTER, av, sv, key, pref,
961             def);
962   }
963
964   /**
965    * get from following(AFTER), first occurence of (FIRST) or previous (BEFORE)
966    * Arg of type a or subval key or preference pref or default def
967    */
968   public static String getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
969           Position pos, ArgValue av, SubVals sv, String key, String pref,
970           String def)
971   {
972     return getFromSubValArgOrPrefWithSubstitutions(null, avm, a, pos, av,
973             sv, key, pref, def);
974   }
975
976   public static String getFromSubValArgOrPrefWithSubstitutions(ArgParser ap,
977           ArgValuesMap avm, Arg a, Position pos, ArgValue av, SubVals sv,
978           String key, String pref, String def)
979   {
980     if (key == null)
981       key = a.getName();
982     String value = null;
983     if (sv != null && sv.has(key) && sv.get(key) != null)
984     {
985       value = ap == null ? sv.get(key)
986               : sv.getWithSubstitutions(ap, avm.getLinkedId(), key);
987     }
988     else if (avm != null && avm.containsArg(a))
989     {
990       if (pos == Position.FIRST && avm.getValue(a) != null)
991         value = avm.getValue(a);
992       else if (pos == Position.BEFORE
993               && avm.getClosestPreviousArgValueOfArg(av, a) != null)
994         value = avm.getClosestPreviousArgValueOfArg(av, a).getValue();
995       else if (pos == Position.AFTER
996               && avm.getClosestNextArgValueOfArg(av, a) != null)
997         value = avm.getClosestNextArgValueOfArg(av, a).getValue();
998
999       // look for allstructures subval for Type.STRUCTURE*
1000       Arg arg = av.getArg();
1001       if (value == null && arg.hasOption(Opt.PRIMARY)
1002               && arg.getType() == Type.STRUCTURE
1003               && !a.hasOption(Opt.PRIMARY) && (a.getType() == Type.STRUCTURE
1004                       || a.getType() == Type.STRUCTUREIMAGE))
1005       {
1006         ArgValue av2 = avm.getArgValueOfArgWithSubValKey(a,
1007                 Arg.ALLSTRUCTURES.getName());
1008         if (av2 != null)
1009         {
1010           value = av2.getValue();
1011         }
1012       }
1013     }
1014     if (value == null)
1015     {
1016       value = pref != null ? Cache.getDefault(pref, def) : def;
1017     }
1018     return value;
1019   }
1020
1021   public static boolean getBoolFromSubValOrArg(ArgValuesMap avm, Arg a,
1022           SubVals sv)
1023   {
1024     return getFromSubValArgOrPref(avm, a, sv, null, null, false);
1025   }
1026
1027   public static boolean getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
1028           SubVals sv, String key, String pref, boolean def)
1029   {
1030     return getFromSubValArgOrPref(avm, a, sv, key, pref, def, false);
1031   }
1032
1033   public static boolean getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
1034           SubVals sv, String key, String pref, boolean def,
1035           boolean invertPref)
1036   {
1037     if ((key == null && a == null) || (sv == null && a == null))
1038       return false;
1039
1040     boolean usingArgKey = false;
1041     if (key == null)
1042     {
1043       key = a.getName();
1044       usingArgKey = true;
1045     }
1046
1047     String nokey = ArgParser.NEGATESTRING + key;
1048
1049     // look for key or nokey in subvals first (if using Arg check options)
1050     if (sv != null)
1051     {
1052       // check for true boolean
1053       if (sv.has(key) && sv.get(key) != null)
1054       {
1055         if (usingArgKey)
1056         {
1057           if (!(a.hasOption(Opt.BOOLEAN) || a.hasOption(Opt.UNARY)))
1058           {
1059             Console.debug(
1060                     "Looking for boolean in subval from non-boolean/non-unary Arg "
1061                             + a.getName());
1062             return false;
1063           }
1064         }
1065         return sv.get(key).toLowerCase(Locale.ROOT).equals("true");
1066       }
1067
1068       // check for negative boolean (subval "no..." will be "true")
1069       if (sv.has(nokey) && sv.get(nokey) != null)
1070       {
1071         if (usingArgKey)
1072         {
1073           if (!(a.hasOption(Opt.BOOLEAN)))
1074           {
1075             Console.debug(
1076                     "Looking for negative boolean in subval from non-boolean Arg "
1077                             + a.getName());
1078             return false;
1079           }
1080         }
1081         return !sv.get(nokey).toLowerCase(Locale.ROOT).equals("true");
1082       }
1083     }
1084
1085     // check argvalues
1086     if (avm != null && avm.containsArg(a))
1087       return avm.getBoolean(a);
1088
1089     // return preference or default
1090     boolean prefVal = pref != null ? Cache.getDefault(pref, def) : false;
1091     return pref != null ? (invertPref ? !prefVal : prefVal) : def;
1092   }
1093
1094   // the following methods look for the "*" linkedId and add the argvalue to all
1095   // linkedId ArgValues if it does.
1096   /**
1097    * This version inserts the subvals sv into all created values
1098    */
1099   private void addValue(String linkedId, Type type, ArgValues avs,
1100           SubVals sv, String v, int argIndex, boolean doSubs)
1101   {
1102     this.argValueOperation(Op.ADDVALUE, linkedId, type, avs, sv, v, false,
1103             argIndex, doSubs);
1104   }
1105
1106   private void addValue(String linkedId, Type type, ArgValues avs, String v,
1107           int argIndex, boolean doSubs)
1108   {
1109     this.argValueOperation(Op.ADDVALUE, linkedId, type, avs, null, v, false,
1110             argIndex, doSubs);
1111   }
1112
1113   private void setBoolean(String linkedId, Type type, ArgValues avs,
1114           boolean b, int argIndex)
1115   {
1116     this.argValueOperation(Op.SETBOOLEAN, linkedId, type, avs, null, null,
1117             b, argIndex, false);
1118   }
1119
1120   private void setNegated(String linkedId, ArgValues avs, boolean b)
1121   {
1122     this.argValueOperation(Op.SETNEGATED, linkedId, null, avs, null, null,
1123             b, 0, false);
1124   }
1125
1126   private void incrementCount(String linkedId, ArgValues avs)
1127   {
1128     this.argValueOperation(Op.INCREMENTCOUNT, linkedId, null, avs, null,
1129             null, false, 0, false);
1130   }
1131
1132   private enum Op
1133   {
1134     ADDVALUE, SETBOOLEAN, SETNEGATED, INCREMENTCOUNT
1135   }
1136
1137   private void argValueOperation(Op op, String linkedId, Type type,
1138           ArgValues avs, SubVals sv, String v, boolean b, int argIndex,
1139           boolean doSubs)
1140   {
1141     // default to merge subvals if subvals are provided
1142     argValueOperation(op, linkedId, type, avs, sv, true, v, b, argIndex,
1143             doSubs);
1144   }
1145
1146   /**
1147    * The following operations look for the "*" and "open*" linkedIds and add the
1148    * argvalue to all appropriate linkedId ArgValues if it does. If subvals are
1149    * supplied, they are inserted into all new set values.
1150    * 
1151    * @param op
1152    *          The ArgParser.Op operation
1153    * @param linkedId
1154    *          The String linkedId from the ArgValuesMap
1155    * @param type
1156    *          The Arg.Type to attach to this ArgValue
1157    * @param avs
1158    *          The ArgValues for this linkedId
1159    * @param sv
1160    *          Use these SubVals on the ArgValue
1161    * @param merge
1162    *          Merge the SubVals with any existing on the value. False will
1163    *          replace unless sv is null
1164    * @param v
1165    *          The value of the ArgValue (may contain subvals).
1166    * @param b
1167    *          The boolean value of the ArgValue.
1168    * @param argIndex
1169    *          The argIndex for the ArgValue.
1170    * @param doSubs
1171    *          Whether to perform substitutions on the subvals and value.
1172    */
1173   private void argValueOperation(Op op, String linkedId, Type type,
1174           ArgValues avs, SubVals sv, boolean merge, String v, boolean b,
1175           int argIndex, boolean doSubs)
1176   {
1177     Arg a = avs.arg();
1178
1179     List<String> wildcardLinkedIds = null;
1180     if (a.hasOption(Opt.ALLOWMULTIID))
1181     {
1182       switch (linkedId)
1183       {
1184       case MATCHALLLINKEDIDS:
1185         wildcardLinkedIds = getLinkedIds();
1186         break;
1187       case MATCHOPENEDLINKEDIDS:
1188         wildcardLinkedIds = this.storedLinkedIds;
1189         break;
1190       }
1191     }
1192
1193     // if we're not a wildcard linkedId and the arg is marked to be stored, add
1194     // to storedLinkedIds
1195     if (linkedId != null && wildcardLinkedIds == null
1196             && a.hasOption(Opt.STORED)
1197             && !storedLinkedIds.contains(linkedId))
1198     {
1199       storedLinkedIds.add(linkedId);
1200     }
1201
1202     // if we are a wildcard linkedId, apply the arg and value to all appropriate
1203     // linkedIds
1204     if (wildcardLinkedIds != null)
1205     {
1206       for (String id : wildcardLinkedIds)
1207       {
1208         // skip incorrectly stored wildcard ids!
1209         if (id == null || MATCHALLLINKEDIDS.equals(id)
1210                 || MATCHOPENEDLINKEDIDS.equals(id))
1211         {
1212           continue;
1213         }
1214         ArgValuesMap avm = linkedArgs.get(id);
1215         // don't set an output if there isn't an input
1216         if (a.hasOption(Opt.REQUIREINPUT)
1217                 && !avm.hasArgWithOption(Opt.INPUT))
1218           continue;
1219
1220         ArgValues tavs = avm.getOrCreateArgValues(a);
1221         switch (op)
1222         {
1223
1224         case ADDVALUE:
1225           String val = v;
1226           if (sv != null)
1227           {
1228             if (doSubs)
1229             {
1230               sv = new SubVals(sv, val, merge);
1231               val = makeSubstitutions(sv.getContent(), id);
1232             }
1233             tavs.addValue(sv, type, val, argIndex, true);
1234           }
1235           else
1236           {
1237             if (doSubs)
1238             {
1239               val = makeSubstitutions(v, id);
1240             }
1241             tavs.addValue(type, val, argIndex, true);
1242           }
1243           finaliseStoringArgValue(id, tavs);
1244           break;
1245
1246         case SETBOOLEAN:
1247           tavs.setBoolean(type, b, argIndex, true);
1248           finaliseStoringArgValue(id, tavs);
1249           break;
1250
1251         case SETNEGATED:
1252           tavs.setNegated(b, true);
1253           break;
1254
1255         case INCREMENTCOUNT:
1256           tavs.incrementCount();
1257           break;
1258
1259         default:
1260           break;
1261
1262         }
1263
1264       }
1265     }
1266     else // no wildcard linkedId -- do it simpler
1267     {
1268       switch (op)
1269       {
1270       case ADDVALUE:
1271         String val = v;
1272         if (sv != null)
1273         {
1274           if (doSubs)
1275           {
1276             val = makeSubstitutions(v, linkedId);
1277             sv = new SubVals(sv, val);
1278           }
1279           avs.addValue(sv, type, val, argIndex, false);
1280         }
1281         else
1282         {
1283           if (doSubs)
1284           {
1285             val = makeSubstitutions(v, linkedId);
1286           }
1287           avs.addValue(type, val, argIndex, false);
1288         }
1289         finaliseStoringArgValue(linkedId, avs);
1290         break;
1291
1292       case SETBOOLEAN:
1293         avs.setBoolean(type, b, argIndex, false);
1294         finaliseStoringArgValue(linkedId, avs);
1295         break;
1296
1297       case SETNEGATED:
1298         avs.setNegated(b, false);
1299         break;
1300
1301       case INCREMENTCOUNT:
1302         avs.incrementCount();
1303         break;
1304
1305       default:
1306         break;
1307       }
1308     }
1309   }
1310
1311   private ArgValuesMap getOrCreateLinkedArgValuesMap(String linkedId)
1312   {
1313     if (linkedArgs.containsKey(linkedId)
1314             && linkedArgs.get(linkedId) != null)
1315       return linkedArgs.get(linkedId);
1316
1317     linkedArgs.put(linkedId, new ArgValuesMap(linkedId));
1318     return linkedArgs.get(linkedId);
1319   }
1320
1321 }